Introduction
fonts-rs is a Rust workspace for integrating font families into GTK4 projects. It provides a modular framework for exporting glyphs as SVG icons, generating GResource bundles, resolving icon names to Unicode codepoints, and rendering text with software rasterization.
Architecture Overview
The workspace is organized into three layers:
- Generic framework -
fonts-rs-model(shared types) andfonts-rs-generator(build-time pipeline) - Font family crates - one crate per font family (e.g.
fonts-rs-doto,fonts-rs-bravura,fonts-rs-seven-segment) - Nerd Fonts integration -
nerd-fonts-model,nerd-fonts-generator, andnerd-fonts-rsfor the legacy Nerd Fonts-specific API
graph TD
subgraph "Generic Framework"
Model["fonts-rs-model<br/>Shared types"]
Gen["fonts-rs-generator<br/>Build pipeline"]
end
subgraph "Font Family Crates"
Doto["fonts-rs-doto"]
Bravura["fonts-rs-bravura"]
SevenSeg["fonts-rs-seven-segment"]
NotoEmoji["fonts-rs-noto-emoji"]
Other["... 10 more"]
end
subgraph "Nerd Fonts"
NFModel["nerd-fonts-model"]
NFGen["nerd-fonts-generator"]
NFRs["nerd-fonts-rs"]
end
Model --> Gen
Model --> Doto
Model --> Bravura
Model --> SevenSeg
Model --> NotoEmoji
Model --> Other
Gen --> Doto
Gen --> Bravura
Gen --> SevenSeg
Gen --> NotoEmoji
Gen --> Other
Model --> NFModel
Gen --> NFGen
NFModel --> NFGen
NFGen --> NFRs
Model --> NFRs
Features
- Modular font family crates - each font family is a separate crate with its own build script, GResource bundle, and runtime API
- Type-safe glyph names -
GlyphName<F>phantom typing prevents mix-ups between font families at compile time - Variable font support - variants via Cargo features with axis-based rendering (e.g. weight, roundness)
- GTK4 integration - GResource registration, icon name resolution, CSS providers
- Software rendering - font loading via
ab_glyphfor headless rendering (seepixel-drawing) - Build-time code generation -
phf::Mapcodepoint maps, Rust constants, and GResource XML generated from font files - Metadata generation - keywords, categories, and aliases for search functionality (e.g. Noto Emoji CLDR annotations)
Available Font Families
| Crate | Font | License | Variants |
|---|---|---|---|
fonts-rs-doto | Doto | OFL-1.1 | Variable (wght, ROND) |
fonts-rs-seven-segment | DSEG7 | OFL-1.1 | Multiple files |
fonts-rs-fourteen-segment | DSEG14 | OFL-1.1 | Multiple files |
fonts-rs-barcode-code39 | Lib Barcode 39 | OFL-1.1 | - |
fonts-rs-barcode-code128 | Lib Barcode 128 | OFL-1.1 | - |
fonts-rs-barcode-ean13 | Lib Barcode EAN13 | OFL-1.1 | - |
fonts-rs-bravura | Bravura | OFL-1.1 | - |
fonts-rs-redacted | Redacted | OFL-1.1 | - |
fonts-rs-dicefont | DiceFont | OFL-1.1 | - |
fonts-rs-cuernavaca | Cuernavaca | OFL-1.1 | - |
fonts-rs-noto-emoji | Noto Emoji | OFL-1.1 | - |
nerd-fonts-rs | Nerd Fonts | MIT | - |
License
MIT for framework code. Font files retain their original licenses (OFL-1.1 for most fonts, MIT for Nerd Fonts). See LICENSE.
Getting Started
Prerequisites
Rust
A working Rust toolchain is required. The project uses Rust Edition 2024.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Minimum Supported Rust Version (MSRV)
The crate declares rust-version = "1.92" in its Cargo.toml. The MSRV is
verified by CI on every push and pull request.
Linux System Dependencies
The gtk feature requires GTK 4 development libraries.
Ubuntu/Debian:
sudo apt-get install -y pkg-config libgtk-4-dev libglib2.0-dev
Fedora:
sudo dnf install -y pkg-config gtk4-devel glib2-devel
Arch Linux:
sudo pacman -S pkgconf gtk4 glib2
No System Dependencies (font loading or web only)
If you only need font loading (render feature) or web CSS (web feature),
no system libraries are required beyond a Rust toolchain.
Installation
From crates.io
Each font family is a separate crate. Add the ones you need:
[dependencies]
# Nerd Fonts (icon name resolution, GTK4 integration)
nerd-fonts-rs = "0.1"
# Specific font families
fonts-rs-doto = "0.1"
fonts-rs-bravura = "0.1"
fonts-rs-seven-segment = "0.1"
fonts-rs-noto-emoji = "0.1"
From Source
git clone https://github.com/smearor/fonts-rs.git
cd fonts-rs
cargo build --release
Feature Selection
Each font family crate provides the same feature gates:
# GTK4 integration (default)
fonts-rs-doto = "0.1"
# GTK4 + software rendering + embedded fonts
fonts-rs-doto = { version = "0.1", features = ["gtk", "render", "embed-fonts"] }
# Software rendering only (no GTK)
fonts-rs-doto = { version = "0.1", default-features = false, features = ["render"] }
# Variable font variant selection (Doto example)
fonts-rs-doto = { version = "0.1", features = ["bold-dot"] }
For Nerd Fonts specifically:
# GTK4 with 4.12+ APIs
nerd-fonts-rs = { version = "0.1", features = ["gtk", "v4_12"] }
# Font loading only (no GTK)
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["render"] }
# Web CSS only
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["web"] }
# Icon metadata (keywords & categories)
nerd-fonts-rs = { version = "0.1", features = ["metadata"] }
Quick Start
Font Family Crate (e.g. Doto)
use fonts_rs_doto::register_glyphs;
use fonts_rs_doto::GlyphNameExt;
use fonts_rs_doto::DotoName;
fn main() {
// Register GResource (gtk feature)
register_glyphs().unwrap();
// Look up a glyph by name
let name = DotoName::new("doto-a".to_string());
if let Some(codepoint) = name.codepoint() {
println!("Glyph codepoint: U+{:04X}", codepoint as u32);
}
}
Nerd Fonts (nerd-fonts-rs)
use nerd_fonts_rs::{init, resolve_icon_codepoint};
fn main() {
// Call once at startup
init(None);
// Resolve an icon name to its Unicode codepoint
if let Some(c) = resolve_icon_codepoint("nf-fa-gamepad") {
println!("Gamepad icon: U+{:04X}", c as u32);
}
}
Running Tests
cargo test
Building the Documentation
cd book
mdbook build
The rendered HTML is placed in book/book/. Open book/book/index.html in a
browser, or run mdbook serve for live preview during development.
Architecture
Workspace Structure
The workspace is organized into 19 crates across three layers:
| Layer | Crates | Role |
|---|---|---|
| Generic framework | fonts-rs-model, fonts-rs-generator | Shared types and build pipeline |
| Font families | fonts-rs-doto, fonts-rs-bravura, … (10 crates) | One crate per font family |
| Nerd Fonts | nerd-fonts-model, nerd-fonts-generator, nerd-fonts-rs | Nerd Fonts-specific integration |
| Applications | nerd-fonts-cheat-sheet, fonts-rs-noto-emoji-cheat-sheet | Demo / cheat sheet apps |
Crate Dependencies
graph TD
Model["fonts-rs-model"] --> Gen["fonts-rs-generator"]
Model --> NFModel["nerd-fonts-model"]
Gen --> NFGen["nerd-fonts-generator"]
NFModel --> NFGen
Gen --> NotoGen["fonts-rs-noto-emoji-generator"]
Model --> Doto["fonts-rs-doto"]
Gen --> Doto
Model --> Bravura["fonts-rs-bravura"]
Gen --> Bravura
Model --> Seven["fonts-rs-seven-segment"]
Gen --> Seven
NotoGen --> NotoEmoji["fonts-rs-noto-emoji"]
NFGen --> NFRs["nerd-fonts-rs"]
Model --> NFRs
NotoEmoji --> NotoCS["fonts-rs-noto-emoji-cheat-sheet"]
NFRs --> NFCS["nerd-fonts-cheat-sheet"]
Build Pipeline
Each font family crate follows the same build pipeline via FontBuild:
flowchart TD
Font["Font file (TTF/OTF)"] --> Hash["FNV-1a hash"]
Hash --> Check{"Hash changed?"}
Check -->|Yes| Export["Export glyphs to SVG"]
Check -->|No| Skip["Skip export"]
Export --> Meta["metadata.json"]
Export --> SVGs["scalable/glyphs/*.svg"]
Export --> XML["icons.gresource.xml"]
Meta --> Codegen["Code generation"]
XML --> GResource["GResource compilation"]
Codegen --> Codemap["$OUT_DIR/codemap.rs (phf::Map)"]
Codegen --> Icons["$OUT_DIR/icons.rs (constants)"]
Codegen --> Variant["$OUT_DIR/variant.rs (variant info)"]
GResource --> Compiled["icons.gresource (binary)"]
Skip --> Codegen
Hash-Based Change Detection
FontBuild computes an FNV-1a hash of the font file and stores it in
resources/.hash. On subsequent builds, the hash is compared to determine
whether re-export is needed. For variable font variants, extra_hash()
adds the variant name to the hash, forcing re-export when the active
variant changes even if the font file is unchanged.
Code Generation
The default code generation produces three files in OUT_DIR:
codemap.rs-phf::Map<char, &str>(codepoint → glyph name) andphf::Map<&str, char>(glyph name → codepoint)icons.rs-pub conststrings for each glyph namevariant.rs-GRESOURCE_PREFIXandGLYPH_PREFIXconstants
Custom code generation is available via FontBuild::run_with().
Font Family Crate Anatomy
Each font family crate has the following structure:
crates/fonts-rs-{name}/
├── Cargo.toml # Features, dependencies, license-file
├── build.rs # Build pipeline (FontBuild + ExportConfig)
├── resources/
│ ├── {font}.ttf # Bundled font file
│ ├── {license}.txt # Font license
│ └── icons.gresource.xml # Generated by build.rs
└── src/
├── lib.rs # Runtime API (register_glyphs, all_glyphs, GlyphNameExt)
├── definition.rs # FontFamilyConfig implementation
├── naming.rs # FontFamily marker enum + GlyphName<F> type alias
├── variant.rs # include!(OUT_DIR/variant.rs)
├── codepoint_map.rs # include!(OUT_DIR/codemap.rs)
├── constants.rs # include!(OUT_DIR/icons.rs)
└── fonts.rs # impl_font_loader! (optional, render feature)
Feature Gates
Each font family crate uses feature gates for optional functionality:
| Feature | Description |
|---|---|
gtk (default) | GResource registration, GTK4 icon name resolution |
render | Font loading via ab_glyph for software rendering |
embed-fonts | Embed font files via include_bytes! instead of disk |
| Variant features | One Cargo feature per font variant (e.g. bold-dot) |
GResource Registration
The build.rs script compiles GResource bundles via glib-build-tools:
icons.gresourcefromresources/icons.gresource.xml- contains all exported SVG glyph filesfont.gresource(optional) fromresources/font.gresource.xml- contains the font file itself for GResource-based font loading
At runtime, register_glyphs() registers the GResource bundle via
gio::resources_register_include!("icons.gresource").
Initialization Flow
sequenceDiagram
participant App as Application
participant Crate as fonts-rs-{name}
participant GResource as GResource
participant CSS as CssProvider
App->>Crate: register_glyphs()
Crate->>GResource: resources_register_include!("icons.gresource")
GResource-->>Crate: OK
Crate-->>App: Ready
Note over App,CSS: For Nerd Fonts (nerd-fonts-rs only)
App->>Crate: init(base_dir)
Crate->>GResource: Register font + icon GResources
Crate->>CSS: Load font_face_css() into CssProvider
Crate->>CSS: Add provider to default display
Crate-->>App: Ready
fonts-rs-model - Generic Model Types
The fonts-rs-model crate provides font-family-agnostic types shared across
all fonts-rs-* crates. These types are the foundation of the modular fonts
framework, enabling any font family crate to use the same build pipeline
without Nerd Fonts dependencies.
FontFamily (Marker Trait)
A sealed marker trait that identifies each font family at compile time.
Each font family crate defines a zero-sized enum implementing FontFamily
and the private Sealed trait. This prevents external crates from defining
their own font family markers.
#![allow(unused)]
fn main() {
pub trait FontFamily: sealed::Sealed {}
}
Used as the phantom type parameter in GlyphName<F>, so that
GlyphName<Doto> and GlyphName<SevenSegment> are distinct types at
compile time - preventing mix-ups between font families.
Example
#![allow(unused)]
fn main() {
use fonts_rs_model::FontFamily;
use fonts_rs_model::sealed;
pub enum Doto {}
impl FontFamily for Doto {}
impl sealed::Sealed for Doto {}
}
FontFamilyConfig (Trait)
Base configuration for all font families. Provides compile-time constants
for GResource prefixes, icon context directories, and codepoint ranges.
Used by both FontDefinition (type-safe glyph names) and ExportConfig
(runtime variant pipeline).
#![allow(unused)]
fn main() {
pub trait FontFamilyConfig {
const FONT_FAMILY_NAME: &'static str;
const FAMILY_DISPLAY_NAME: &'static str;
const GRESOURCE_PREFIX: &'static str;
const ICONS_CONTEXT: &'static str = ICONS_CONTEXT_GLYPHS;
const CODEPOINT_RANGES: &[CodePointRange] = &[BMP_RANGE];
fn icons_dir(output_dir: &Path) -> PathBuf { ... }
fn prepare_icons_dir(output_dir: &Path) -> std::io::Result<PathBuf> { ... }
fn icons_resource_prefix() -> String { ... }
}
}
Constants
FONT_FAMILY_NAME: Slug for GResource prefix and glyph names (e.g."doto","dseg7")FAMILY_DISPLAY_NAME: Human-readable name for build logs (e.g."Doto","DSEG7")GRESOURCE_PREFIX: Full GResource prefix (e.g./io/smearor/fonts/doto)ICONS_CONTEXT: Subdirectory afterscalable/- default"glyphs", override e.g."emoji"for Noto EmojiCODEPOINT_RANGES: Unicode ranges for reverse cmap probing - default BMP, override e.g.ASCII_PRINTABLE_RANGEfor barcode fonts
Methods
prepare_icons_dir: Removes thescalable/{context}/directory and recreates it (avoids stale SVGs from previous builds)icons_dir: Returns the path toscalable/{context}/within the output directory
FontDefinition (Trait)
Extends FontFamilyConfig with type-safe glyph name processing and a
default export_glyphs pipeline. Used by font families with semantic
PostScript glyph names (e.g. Barcode, Seven-Segment).
#![allow(unused)]
fn main() {
pub trait FontDefinition: FontFamilyConfig {
type Name: AsRef<str> + Clone + Ord + Serialize + for<'a> Deserialize<'a>;
type Family: FontFamily;
fn normalize_name(raw_glyph_name: &str) -> Option<Self::Name>;
fn should_skip(raw_glyph_name: &str) -> bool { ... }
fn generate_gresource_xml(...) -> std::io::Result<()> { ... }
fn export_glyphs(font_path: &Path, output_dir: &Path) -> std::io::Result<usize> { ... }
}
}
Associated Types
Name: Glyph name type -GlyphName<Self::Family>for simple families,IconNamefor Nerd FontsFamily: Font family marker (e.g.Doto,SevenSegment)
Methods
normalize_name: Raw glyph name → normalized name (e.g."zero"→"dseg7-0")should_skip: Filter for glyph names to skip (default:.,uni,u- overridable)export_glyphs: Default pipeline: SVG export,metadata.json,icons.gresource.xml
FontVariant
Identifies a font variant (e.g. "classic-regular", "bold-dot") and
describes how it is realized: separate file, variable font axes, or both.
#![allow(unused)]
fn main() {
pub struct FontVariant {
name: &'static str,
variant_type: FontVariantType,
}
}
Constructors
#![allow(unused)]
fn main() {
// Separate file, no axes
FontVariant::file("classic-regular", "DSEG7Classic-Regular");
// Shared file, axes only (variable font)
FontVariant::axes("bold-dot", &[AxisValue::new("wght", 700.0), AxisValue::new("ROND", 100.0)]);
// Separate file with additional axes
FontVariant::file_with_axes("bold-extended", "MyFont-Bold", &[AxisValue::new("wdth", 125.0)]);
}
Methods
is_active(): Checks theCARGO_FEATURE_{NAME}environment variableslug(): Variant name with_instead of-(for GResource paths)axis_values(): Returns the axis configurationfont_file(): Returns the font file (if separate)
FontVariantType
Describes how a variant is realized:
#![allow(unused)]
fn main() {
pub struct FontVariantType {
pub font_file: Option<FontFile>,
pub axis_values: AxisValues,
}
}
file(): Separate file, no axesaxes(): Shared file, axes only (variable font)file_with_axes(): Separate file with additional axes
Axes (Axis, AxisValue, AxisValues)
Variable font axis identification and values:
#![allow(unused)]
fn main() {
pub struct Axis(&'static str); // e.g. "wght", "ROND"
pub struct AxisValue {
pub axis: Axis,
pub value: f32,
}
pub struct AxisValues(&'static [AxisValue]);
}
AxisValues::EMPTY- default location (no axes)AxisValuesisconst-compatible forVARIANTSarrays inbuild.rsDerefto[AxisValue]for iteration
CodePointRange
An inclusive Unicode range for reverse cmap probing:
#![allow(unused)]
fn main() {
pub struct CodePointRange {
start: CodePoint,
end: CodePoint,
}
}
Predefined constants in fonts-rs-model:
BMP_RANGE-U+0000–U+FFFF(Default)ASCII_PRINTABLE_RANGE-U+0020–U+007EPUA_RANGE-U+E000–U+F8FFSUPPLEMENTARY_PUA_RANGE-U+F0001–U+10FFFF
Codepoint Map Types
Newtype wrappers around HashMap for type-safe lookups:
CodePointCategoryMap-HashMap<CodePoint, String>: codepoint → category (e.g. fromemoji-test.txt)CodePointKeywordMap-HashMap<CodePoint, Vec<String>>: codepoint → keyword list (e.g. from CLDR annotations)CodePointNameMap-HashMap<CodePoint, String>: codepoint → canonical name (e.g. from CLDRttsfields)GlyphNameMap-HashMap<String, CodePoint>: glyph name → codepoint (e.g. from SMuFLglyphnames.json). ImplementsDeserializefor the SMuFL JSON format{ "glyphName": { "codepoint": "U+E050", ... } }.
GlyphEntry<N>
Metadata for a single exported glyph:
#![allow(unused)]
fn main() {
pub struct GlyphEntry<N: AsRef<str>> {
pub code: Option<CodePoint>,
pub name: N,
pub file: PathBuf,
pub resource_path: ResourcePath,
}
}
GlyphEntry::new(code, name, gresource_prefix, icons_context)- constructsfileandresource_pathautomatically from prefix, context, and name- Generic over
N:GlyphName<F>for type-safe families,Stringfor theExportConfigpipeline,IconNamefor Nerd Fonts
GlyphName<F>
Phantom-typed glyph name newtype:
#![allow(unused)]
fn main() {
pub struct GlyphName<F: FontFamily> {
name: String,
_marker: PhantomData<F>,
}
}
Implements AsRef<str>, Display, Serialize, Deserialize, Hash, Eq,
Ord. Construction via GlyphName::new(String) after normalization.
Each font family crate defines a type alias:
#![allow(unused)]
fn main() {
pub type DotoName = GlyphName<Doto>;
}
FontFile
A font file name without extension, e.g. "DSEG7Classic-Regular", "Doto".
Wraps a &'static str for type-safe font file references in build scripts.
ResourcePath
A GResource resource path (e.g. /io/smearor/fonts/doto/scalable/glyphs/doto-a.svg).
Used by GlyphEntry to store the GResource URI for each exported glyph.
fonts-rs-generator - Build-Time Pipeline
The fonts-rs-generator crate provides the build-time pipeline for exporting
glyphs from TTF/OTF font files, generating codepoint maps, compiling
GResource bundles, and producing Rust source constants. It is used as a
build-dependency by every font family crate.
ExportConfig<X: FontFamilyConfig>
Runtime configuration for glyph export, decoupled from FontDefinition.
Enables variant-specific parameters at build time without a separate
FontDefinition impl per variant.
#![allow(unused)]
fn main() {
pub struct ExportConfig<X: FontFamilyConfig> {
pub gresource_prefix: String,
pub glyph_name_prefix: String,
pub axes: AxisValues,
pub name_filter: Option<fn(&str) -> bool>,
pub family_display_name: String,
_marker: PhantomData<X>,
}
}
Constructors
ExportConfig::new()- without a variant, usesX::GRESOURCE_PREFIXandX::FONT_FAMILY_NAMEExportConfig::with_variant(variant)- with a variant, constructsgresource_prefixas{X::GRESOURCE_PREFIX}/{variant_slug}andglyph_name_prefixas{X::FONT_FAMILY_NAME}-{variant}
Methods
export_glyphs(font_path, output_dir)- standard export: iterates over all glyph IDs, normalizes names to{glyph_name_prefix}-{kebab}, renders SVGs with axis locationexport_glyphs_by_name_map(font_path, output_dir, name_map)- export via external name→codepoint map (e.g. SMuFLglyphnames.json): for fonts without PostScript glyph nameswrite_variant_info()- writesOUT_DIR/variant.rswithGRESOURCE_PREFIXandGLYPH_PREFIXconstantsgenerate_gresource_xml(entries, path)- generatesicons.gresource.xmlwithquick-xmlserialization
export_glyphs() (Two Variants)
There are two distinct export_glyphs methods in the framework:
FontDefinition::export_glyphs (trait default method)
Uses Self::normalize_name for type-safe glyph names. Iterates over all
glyph IDs, calls should_skip and normalize_name, renders SVGs without
axis location (default location).
ExportConfig::export_glyphs (struct method)
Uses normalize_to_kebab + glyph_name_prefix for string-based names.
Renders SVGs with axis location (glyph_to_svg_full_height_at).
Supports name_filter for custom glyph filtering.
Output
Both produce:
<output_dir>/scalable/{context}/*.svg<output_dir>/metadata.json<output_dir>/icons.gresource.xml
write_variant_info()
Writes OUT_DIR/variant.rs with two constants:
#![allow(unused)]
fn main() {
pub const GRESOURCE_PREFIX: &str = "/io/smearor/fonts/doto/regular_medium";
pub const GLYPH_PREFIX: &str = "doto-regular-medium";
}
Included by lib.rs via include!(concat!(env!("OUT_DIR"), "/variant.rs")).
Enables the runtime API to construct GResource paths and glyph names with
the active variant prefix.
detect_and_get_active_variant()
On VariantList<X: FontFamilyConfig>:
#![allow(unused)]
fn main() {
pub fn detect_and_get_active_variant(&self, default_index: usize) -> miette::Result<&'a FontVariant>
}
Scans CARGO_FEATURE_{NAME} environment variables for each variant.
Returns the active variant or default_index if none is active. Returns
an error if more than one variant is active. Uses X::FAMILY_DISPLAY_NAME
for diagnostic messages.
FontBuild (Builder)
Builder for the common build.rs pipeline. Handles hash-based change
detection, conditional glyph export, GResource compilation, and code
generator invocation.
#![allow(unused)]
fn main() {
FontBuild::new(&font_path)
.extra_hash(entry.as_str()) // for variable font variants
.compile_font_gresource() // optional: font.gresource
.rerun_if_changed(path) // additional cargo:rerun-if-changed
.additional_gresource(xml, out) // additional GResource bundles
.run(|font_path, resources_dir| {
config.export_glyphs(font_path, resources_dir)
})
.map_err(|e| miette::miette!("{e}"))?;
}
Builder Methods
extra_hash: Additional hash component (e.g. variant name) - forces re-export on variant change even if the font file is unchangedcompile_font_gresource: Also compilefont.gresourcein addition toicons.gresourcererun_if_changed: Add extracargo:rerun-if-changedpathsadditional_gresource: Register additional GResource bundles to compile
Run Methods
run: Export closure + default code generation (CodemapGenerator+RustConstantsGenerator)run_with: Export closure + custom code generation closure- Hash is computed as FNV-1a of the font file and stored in
resources/.hash
impl_font_loader! Macro
Generates a font() -> Option<&'static ab_glyph::FontVec> function that
loads a TTF font file and caches it in a OnceLock.
Two variants:
#![allow(unused)]
fn main() {
// Literal: relative to resources/
fonts_rs_generator::impl_font_loader!("Doto.ttf");
// Env var: absolute path set by build.rs via cargo:rustc-env
fonts_rs_generator::impl_font_loader!(env: "DOTO_FONT_PATH");
}
With the embed-fonts feature, include_bytes! is used; otherwise
std::fs::read.
MetadataGenerator (Trait)
Trait for generating phf::Map metadata tables (keywords, categories,
aliases) at build time.
#![allow(unused)]
fn main() {
pub trait MetadataGenerator {
type Name: AsRef<str>;
fn keywords_for(&self, entry: &GlyphEntry<Self::Name>) -> Vec<String>;
fn categories_for(&self, entry: &GlyphEntry<Self::Name>) -> Vec<String>;
fn aliases(&self) -> Vec<(String, String)> { Vec::new() }
fn deduplicate_statics() -> bool { false }
fn generate_keywords(&self, entries: &[GlyphEntry<Self::Name>]) -> String { ... }
fn generate_categories(&self, entries: &[GlyphEntry<Self::Name>]) -> String { ... }
fn generate_aliases(&self) -> String { ... }
fn run(&self, entries: &[GlyphEntry<Self::Name>]) -> Result<(), std::io::Error> { ... }
}
}
deduplicate_statics() = true: Extracts repeated value lists intostaticconstants (e.g.static KW_0: &[GlyphKeyword] = &[...]) - reduces binary size for large maps with many duplicatesrun(): Writeskeywords.rs,categories.rs,aliases.rstoOUT_DIR
Example implementation: NotoEmojiMetadataGenerator in
fonts-rs-noto-emoji-generator uses CodePointKeywordMap and
CodePointCategoryMap as data sources.
VariantList<X: FontFamilyConfig>
A typed list of font variants, generic over the font family config. Provides methods to detect the active variant from Cargo features.
#![allow(unused)]
fn main() {
const VARIANTS: VariantList<DotoConfig> = VariantList::new(&[
FontVariant::axes("regular-medium", &[AxisValue::new("wght", 500.0), AxisValue::new("ROND", 50.0)]),
FontVariant::axes("bold-dot", &[AxisValue::new("wght", 700.0), AxisValue::new("ROND", 100.0)]),
]);
let entry = VARIANTS.detect_and_get_active_variant(0)?;
}
FontVariantExt (Extension Trait)
Extension trait providing a method to get the font file path from a
FontVariant, handling errors if no font file is associated.
set_font_path_env() Helper
Sets cargo:rustc-env with the absolute font path for include_bytes!
in lib.rs. Constructs the absolute path from CARGO_MANIFEST_DIR and
the given relative font path.
#![allow(unused)]
fn main() {
set_font_path_env("DOTO_FONT_PATH", &font_path)?;
}
build_constants Module
Provides common constants for build scripts:
RESOURCES_DIR- theresources/directory pathMETADATA_PATH- path toresources/metadata.jsonHASH_PATH- path toresources/.hashICONS_GRESOURCE_XML- path toresources/icons.gresource.xmlICONS_GRESOURCE- path to compiledicons.gresourceFONT_GRESOURCE_XML- path toresources/font.gresource.xmlFONT_GRESOURCE- path to compiledfont.gresource
Build Script & Runtime API Patterns
Each font family crate follows a consistent structure with a build.rs
for build-time glyph export and a lib.rs for runtime API. This page
documents the two main build.rs patterns and the standard lib.rs
layout.
Crate File Structure
crates/fonts-rs-{name}/
├── Cargo.toml
├── build.rs
├── resources/
│ ├── {font}.ttf or .otf
│ ├── {license}.txt
│ └── icons.gresource.xml (generated by build.rs)
└── src/
├── lib.rs
├── definition.rs
├── naming.rs
├── variant.rs (generated by build.rs)
├── codepoint_map.rs (generated by build.rs)
├── constants.rs (generated by build.rs)
└── fonts.rs (optional, render feature)
definition.rs - FontFamilyConfig Implementation
Each font family crate defines a definition.rs with a
FontFamilyConfig implementation:
#![allow(unused)]
fn main() {
use const_format::concatcp;
use fonts_rs_generator::FontFamilyConfig;
use fonts_rs_model::CodePointRange;
use fonts_rs_model::GRESOURCE_BASE_PREFIX;
use fonts_rs_model::ASCII_PRINTABLE_RANGE;
pub struct DotoConfig;
impl FontFamilyConfig for DotoConfig {
const FONT_FAMILY_NAME: &'static str = "doto";
const FAMILY_DISPLAY_NAME: &'static str = "Doto";
const GRESOURCE_PREFIX: &'static str = concatcp!(GRESOURCE_BASE_PREFIX, "/", DotoConfig::FONT_FAMILY_NAME);
const CODEPOINT_RANGES: &[CodePointRange] = ASCII_PRINTABLE_RANGE;
}
}
The definition.rs is included by both build.rs (via #[path = ...])
and lib.rs (via mod definition).
build.rs Patterns
There are two main patterns for build.rs, depending on whether the font
has variants.
Pattern 1: Variable Font with Variants (e.g. Doto)
Used for variable fonts where the same TTF file is rendered at different axis locations, or for font families with multiple separate font files. Variants are selected via Cargo features at build time.
use std::path::Path;
use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::VariantList;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::AxisValue;
use fonts_rs_model::FontVariant;
#[path = "src/definition.rs"]
mod definition;
use definition::DotoConfig;
const FONT_FILE: &str = "Doto.ttf";
const VARIANTS: VariantList<DotoConfig> = VariantList::new(&[
FontVariant::axes("regular-medium", &[AxisValue::new("wght", 500.0), AxisValue::new("ROND", 50.0)]),
FontVariant::axes("bold-dot", &[AxisValue::new("wght", 700.0), AxisValue::new("ROND", 100.0)]),
// ...
]);
fn main() -> miette::Result<()> {
let entry = VARIANTS.detect_and_get_active_variant(12)?;
let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);
eprintln!("build.rs: active variant: {} -> {}", entry, font_path.display());
set_font_path_env("DOTO_FONT_PATH", &font_path)?;
let config = ExportConfig::<DotoConfig>::with_variant(*entry);
FontBuild::new(&font_path)
.extra_hash(entry.as_str())
.run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
.map_err(|e| miette::miette!("{e}"))?;
config.write_variant_info()?;
Ok(())
}
Key points:
VariantList::new(&[...])defines all variants as a const arraydetect_and_get_active_variant(default_index)scans Cargo featuresextra_hash(entry.as_str())forces re-export when the variant changesExportConfig::with_variant(*entry)creates variant-specific configwrite_variant_info()generatesOUT_DIR/variant.rs
Pattern 2: Static Font with External Name Map (e.g. Bravura)
Used for fonts without PostScript glyph names (e.g. SMuFL fonts with
post table version 3.0). Glyph names come from an external metadata
file (e.g. glyphnames.json).
use std::path::Path;
use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::GlyphNameMap;
#[path = "src/definition.rs"]
mod definition;
use definition::BravuraConfig;
const FONT_FILE: &str = "Bravura.otf";
const GLYPHNAMES_FILE: &str = "glyphnames.json";
fn parse_glyphnames(json: &str) -> miette::Result<GlyphNameMap> {
serde_json::from_str(json).map_err(|e| miette::miette!("Failed to parse glyphnames.json: {e}"))
}
fn main() -> miette::Result<()> {
let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);
let glyphnames_path = Path::new(build_constants::RESOURCES_DIR).join(GLYPHNAMES_FILE);
eprintln!("build.rs: exporting glyphs from {}", font_path.display());
set_font_path_env("BRAVURA_FONT_PATH", &font_path)?;
let glyphnames_json = std::fs::read_to_string(&glyphnames_path)
.map_err(|e| miette::miette!("Failed to read {}: {e}", glyphnames_path.display()))?;
let name_map = parse_glyphnames(&glyphnames_json)?;
eprintln!("build.rs: parsed {} SMuFL glyph names", name_map.len());
let config = ExportConfig::<BravuraConfig>::new();
FontBuild::new(&font_path)
.run(|font_path, resources_dir| {
config.export_glyphs_by_name_map(font_path, resources_dir, &name_map)
})
.map_err(|e| miette::miette!("{e}"))?;
config.write_variant_info()?;
Ok(())
}
Key points:
ExportConfig::new()- no variant, uses family config defaultsexport_glyphs_by_name_map()- uses external name→codepoint mapping- No
extra_hashneeded (no variant selection)
lib.rs - Runtime API
Each font family crate has a lib.rs with the following structure:
#![allow(unused)]
fn main() {
pub mod codepoint_map; // include!(concat!(env!("OUT_DIR"), "/codemap.rs"))
pub mod constants; // include!(concat!(env!("OUT_DIR"), "/icons.rs"))
pub mod naming; // FontFamily marker + GlyphName<F> type alias
pub mod variant; // include!(concat!(env!("OUT_DIR"), "/variant.rs"))
#[cfg(feature = "render")]
pub mod fonts; // impl_font_loader!(env: "FONT_PATH")
use fonts_rs_model::CodePoint;
// Re-export key types
pub use naming::DotoName;
// GResource registration (gtk feature)
#[cfg(feature = "gtk")]
pub fn register_glyphs() -> Result<(), gio::glib::Error> {
gio::resources_register_include!("icons.gresource")?;
Ok(())
}
// Codepoint lookup
pub trait GlyphNameExt {
fn codepoint(&self) -> Option<CodePoint>;
}
impl GlyphNameExt for DotoName {
fn codepoint(&self) -> Option<CodePoint> {
let key = self.as_ref();
if let Some(ch) = codepoint_map::REVERSE_GLYPHS.get(key).copied() {
return Some(CodePoint::from(ch));
}
let full = format!("{}-{}", variant::GLYPH_PREFIX, key);
codepoint_map::REVERSE_GLYPHS.get(&full).copied().map(CodePoint::from)
}
}
// Iterator over all glyphs
pub fn all_glyphs() -> impl Iterator<Item = (char, &'static str)> {
codepoint_map::GLYPHS.entries().map(|(c, name)| (*c, *name))
}
}
Generated Modules
Three modules are generated by build.rs into OUT_DIR:
codepoint_map.rs-phf::MapwithGLYPHS(char → name) andREVERSE_GLYPHS(name → char) mapsicons.rs-pub conststrings for each glyph namevariant.rs-GRESOURCE_PREFIXandGLYPH_PREFIXconstants
naming.rs - Font Family Marker
#![allow(unused)]
fn main() {
use fonts_rs_model::FontFamily;
use fonts_rs_model::GlyphName;
use fonts_rs_model::sealed;
pub enum Doto {}
impl FontFamily for Doto {}
impl sealed::Sealed for Doto {}
pub type DotoName = GlyphName<Doto>;
}
variant.rs - Generated Variant Info
#![allow(unused)]
fn main() {
// @generated by build.rs - do not edit
/// GResource prefix for Doto.
pub const GRESOURCE_PREFIX: &str = "/io/smearor/fonts/doto/regular_medium";
/// Glyph name prefix for Doto.
pub const GLYPH_PREFIX: &str = "doto-regular-medium";
}
fonts.rs - Font Loading (Optional)
#![allow(unused)]
fn main() {
fonts_rs_generator::impl_font_loader!(env: "DOTO_FONT_PATH");
}
Generates a font() function that loads the TTF file from the path
set by build.rs via cargo:rustc-env. Cached in OnceLock.
Adding a New Font Family
This guide walks through creating a new font family crate in the
fonts-rs workspace. The process is designed to require minimal
boilerplate - most of the build pipeline is handled by the generic
framework in fonts-rs-generator.
Overview
A font family crate integrates a TTF/OTF font into the fonts-rs
ecosystem, providing:
- Build-time glyph export to SVG files
- GResource bundle compilation for GTK
- Codepoint map generation (
phf::Map) - Runtime glyph name resolution and GResource registration
- Optional software rendering via
ab_glyph
Step 1: Create the Crate
Create a new directory under crates/:
crates/fonts-rs-{name}/
├── Cargo.toml
├── build.rs
├── resources/
│ └── {font}.ttf
└── src/
├── lib.rs
├── definition.rs
├── naming.rs
├── variant.rs
├── codepoint_map.rs
├── constants.rs
└── fonts.rs (optional)
Step 2: Bundle the Font File
Place the TTF/OTF font file and its license into resources/:
resources/
├── MyFont-Regular.ttf
└── OFL.txt (or appropriate license)
Step 3: Write definition.rs
Implement FontFamilyConfig with the family’s constants:
#![allow(unused)]
fn main() {
//! `FontFamilyConfig` for the MyFont font family.
use const_format::concatcp;
use fonts_rs_generator::FontFamilyConfig;
use fonts_rs_model::CodePointRange;
use fonts_rs_model::GRESOURCE_BASE_PREFIX;
use fonts_rs_model::PUA_RANGE;
pub struct MyFontConfig;
impl FontFamilyConfig for MyFontConfig {
const FONT_FAMILY_NAME: &'static str = "myfont";
const FAMILY_DISPLAY_NAME: &'static str = "MyFont";
const GRESOURCE_PREFIX: &'static str = concatcp!(GRESOURCE_BASE_PREFIX, "/", MyFontConfig::FONT_FAMILY_NAME);
const CODEPOINT_RANGES: &[CodePointRange] = &[PUA_RANGE];
}
}
Choose CODEPOINT_RANGES based on the font’s Unicode coverage:
BMP_RANGE- full BMP (default, most fonts)ASCII_PRINTABLE_RANGE- barcode fonts, seven-segment display fontsPUA_RANGE- SMuFL music fonts, icon fonts in the Private Use Area
Step 4: Write naming.rs
Define the FontFamily marker enum and GlyphName<F> type alias:
#![allow(unused)]
fn main() {
//! Font family marker type for MyFont.
use fonts_rs_model::FontFamily;
use fonts_rs_model::GlyphName;
use fonts_rs_model::sealed;
/// Marker type identifying MyFont in `GlyphName<MyFont>`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum MyFont {}
impl FontFamily for MyFont {}
impl sealed::Sealed for MyFont {}
/// Convenience type alias for MyFont glyph names.
pub type MyFontName = GlyphName<MyFont>;
}
Step 5: Write build.rs
Choose the pattern that matches your font:
Variable Font with Variants
If the font has multiple variants (e.g. weight/roundness combinations),
use VariantList + ExportConfig::with_variant:
use std::path::Path;
use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::VariantList;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::AxisValue;
use fonts_rs_model::FontVariant;
#[path = "src/definition.rs"]
mod definition;
use definition::MyFontConfig;
const FONT_FILE: &str = "MyFont.ttf";
const VARIANTS: VariantList<MyFontConfig> = VariantList::new(&[
FontVariant::axes("regular", &[AxisValue::new("wght", 400.0)]),
FontVariant::axes("bold", &[AxisValue::new("wght", 700.0)]),
]);
fn main() -> miette::Result<()> {
let entry = VARIANTS.detect_and_get_active_variant(0)?;
let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);
set_font_path_env("MYFONT_FONT_PATH", &font_path)?;
let config = ExportConfig::<MyFontConfig>::with_variant(*entry);
FontBuild::new(&font_path)
.extra_hash(entry.as_str())
.run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
.map_err(|e| miette::miette!("{e}"))?;
config.write_variant_info()?;
Ok(())
}
Static Font (No Variants)
If the font has a single style, use ExportConfig::new():
use std::path::Path;
use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
#[path = "src/definition.rs"]
mod definition;
use definition::MyFontConfig;
const FONT_FILE: &str = "MyFont-Regular.ttf";
fn main() -> miette::Result<()> {
let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);
set_font_path_env("MYFONT_FONT_PATH", &font_path)?;
let config = ExportConfig::<MyFontConfig>::new();
FontBuild::new(&font_path)
.run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
.map_err(|e| miette::miette!("{e}"))?;
config.write_variant_info()?;
Ok(())
}
Font with External Name Map
If the font lacks PostScript glyph names (e.g. SMuFL fonts), use
export_glyphs_by_name_map with an external metadata file:
#![allow(unused)]
fn main() {
use fonts_rs_model::GlyphNameMap;
// Parse external metadata (e.g. SMuFL glyphnames.json)
let name_map: GlyphNameMap = serde_json::from_str(&json)?;
let config = ExportConfig::<MyFontConfig>::new();
FontBuild::new(&font_path)
.run(|font_path, resources_dir| {
config.export_glyphs_by_name_map(font_path, resources_dir, &name_map)
})
.map_err(|e| miette::miette!("{e}"))?;
}
Step 6: Write lib.rs
#![allow(unused)]
fn main() {
//! MyFont font integration for GTK 4.
pub mod codepoint_map;
pub mod constants;
pub mod naming;
pub mod variant;
#[cfg(feature = "render")]
pub mod fonts;
use fonts_rs_model::CodePoint;
pub use naming::MyFontName;
/// Type alias for this font family's glyph name type.
pub type FamilyName = MyFontName;
/// Extension trait adding codepoint lookup to `GlyphName<MyFont>`.
pub trait GlyphNameExt {
fn codepoint(&self) -> Option<CodePoint>;
}
impl GlyphNameExt for MyFontName {
fn codepoint(&self) -> Option<CodePoint> {
let key = self.as_ref();
if let Some(ch) = codepoint_map::REVERSE_GLYPHS.get(key).copied() {
return Some(CodePoint::from(ch));
}
let full = format!("{}-{}", variant::GLYPH_PREFIX, key);
codepoint_map::REVERSE_GLYPHS.get(&full).copied().map(CodePoint::from)
}
}
/// Registers embedded glyphs as a GResource.
#[cfg(feature = "gtk")]
pub fn register_glyphs() -> Result<(), gio::glib::Error> {
gio::resources_register_include!("icons.gresource")?;
Ok(())
}
/// Returns an iterator over all known glyphs.
pub fn all_glyphs() -> impl Iterator<Item = (char, &'static str)> {
codepoint_map::GLYPHS.entries().map(|(c, name)| (*c, *name))
}
}
Generated Module Stubs
Create these stub files that include! the generated code:
src/codepoint_map.rs:
#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/codemap.rs"));
}
src/constants.rs:
#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/icons.rs"));
}
src/variant.rs:
#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/variant.rs"));
}
src/fonts.rs (optional, for render feature):
#![allow(unused)]
fn main() {
fonts_rs_generator::impl_font_loader!(env: "MYFONT_FONT_PATH");
}
Step 7: Configure Cargo.toml
[package]
name = "fonts-rs-myfont"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
description = "MyFont font integration for GTK 4 and pixel-drawing"
publish = true
# Font license
license-file = "resources/OFL.txt"
include = [
"src/**/*.rs",
"build.rs",
"resources/*.ttf",
"resources/OFL.txt",
"resources/icons.gresource.xml",
"Cargo.toml",
]
[features]
default = ["gtk"]
# Variant features (if applicable)
regular = []
bold = []
gtk = ["dep:gtk4", "dep:gio"]
render = ["dep:ab_glyph"]
embed-fonts = []
[dependencies]
fonts-rs-generator.workspace = true
fonts-rs-model.workspace = true
gio = { workspace = true, optional = true }
gtk4 = { workspace = true, optional = true }
ab_glyph = { workspace = true, optional = true }
phf.workspace = true
tracing.workspace = true
[build-dependencies]
fonts-rs-generator.workspace = true
fonts-rs-model.workspace = true
const_format.workspace = true
miette.workspace = true
[lints]
workspace = true
Step 8: Update Workspace Cargo.toml
Add the new crate to the workspace members and dependencies:
[workspace]
members = [
# ...
"crates/fonts-rs-myfont",
]
[workspace.dependencies]
# ...
fonts-rs-myfont = { version = "0.1.0", path = "crates/fonts-rs-myfont" }
Step 9: Write Tests
Add tests to lib.rs to verify the export:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_glyphs_is_not_empty() {
let count = all_glyphs().count();
assert!(count > 0, "MyFont should export at least one glyph");
}
#[test]
fn all_glyphs_names_start_with_family_prefix() {
for (_, name) in all_glyphs() {
assert!(
name.starts_with("myfont-"),
"glyph name '{name}' should start with 'myfont-'"
);
}
}
#[test]
fn all_glyphs_names_are_lowercase() {
for (_, name) in all_glyphs() {
assert_eq!(*name, name.to_lowercase(), "glyph name '{name}' should be lowercase");
}
}
#[test]
fn all_glyphs_names_contain_no_underscores() {
for (_, name) in all_glyphs() {
assert!(!name.contains('_'), "glyph name '{name}' should not contain underscores");
}
}
#[test]
fn all_glyphs_codepoints_are_unique() {
let codepoints: Vec<char> = all_glyphs().map(|(c, _)| c).collect();
let unique: std::collections::HashSet<char> = codepoints.iter().copied().collect();
assert_eq!(codepoints.len(), unique.len(), "all codepoints should be unique");
}
}
}
Verification
Build the crate and run tests:
cargo build -p fonts-rs-myfont
cargo test -p fonts-rs-myfont
cargo clippy -p fonts-rs-myfont -- -D warnings
cargo fmt -p fonts-rs-myfont -- --check
Check the generated output:
ls resources/scalable/glyphs/*.svg | wc -l # glyph count
cat resources/metadata.json | head -20 # metadata preview
Glyph Resolution
Each font family crate provides glyph name resolution - mapping human-readable glyph names to Unicode codepoints and vice versa.
Font Family Crates
Font family crates use GlyphName<F> (phantom-typed) for type-safe glyph
name resolution. Each crate defines a GlyphNameExt trait with a
codepoint() method:
#![allow(unused)]
fn main() {
use fonts_rs_doto::GlyphNameExt;
use fonts_rs_doto::DotoName;
// Look up a glyph by name
let name = DotoName::new("doto-a".to_string());
if let Some(codepoint) = name.codepoint() {
println!("Glyph codepoint: U+{:04X}", codepoint as u32);
}
}
The codepoint() method looks up the glyph name in the generated
phf::Map (REVERSE_GLYPHS), trying both the bare name and the
prefixed name ({GLYPH_PREFIX}-{name}).
Iterating All Glyphs
#![allow(unused)]
fn main() {
use fonts_rs_doto::all_glyphs;
for (codepoint, name) in all_glyphs() {
println!("U+{:04X} → {}", codepoint as u32, name);
}
}
GResource Path Resolution
Each glyph has a GResource path that can be used with GTK:
#![allow(unused)]
fn main() {
// GResource path format: {GRESOURCE_PREFIX}/scalable/{ICONS_CONTEXT}/{glyph_name}.svg
// e.g. /io/smearor/fonts/doto/scalable/glyphs/doto-a.svg
}
Nerd Fonts (nerd-fonts-rs)
Nerd Fonts use a different naming convention. Icon names follow the pattern
nf-{prefix}-{name}, for example:
nf-fa-gamepad- Font Awesome gamepad iconnf-md-cube- Material Design cube iconnf-linux-tux- Linux Tux icon
The resolve_icon_codepoint function:
- Normalizes the input to kebab-case, lower-case
- Appends
-symbolicsuffix if not already present (GTK symbolic icon convention) - Looks up the normalized name in the vendored codepoint map
- Returns the Unicode character if found
Usage
#![allow(unused)]
fn main() {
use nerd_fonts_rs::resolve_icon_codepoint;
// Basic resolution
let codepoint = resolve_icon_codepoint("nf-fa-gamepad");
assert_eq!(codepoint, Some('\u{F11B}'));
// Underscores are normalized to hyphens
let codepoint = resolve_icon_codepoint("nf_linux_tux");
assert_eq!(codepoint, Some('\u{F31A}'));
// Case-insensitive
let codepoint = resolve_icon_codepoint("NF-FA-GAMEPAD");
assert_eq!(codepoint, Some('\u{F11B}'));
// Already has -symbolic suffix
let codepoint = resolve_icon_codepoint("nf-fa-gamepad-symbolic");
assert_eq!(codepoint, Some('\u{F11B}'));
// Unknown icon returns None
let codepoint = resolve_icon_codepoint("nf-nonexistent-icon-xyz");
assert_eq!(codepoint, None);
}
GTK Icon Name Resolution
The gtk module provides resolve_gtk_nerd_icon which converts CSS class names
(like nf-fa-gamepad or fa-gamepad) into GTK icon names that
gtk4::Image::from_icon_name understands.
The vendored icon GResource registers SVG icons under the path
/io/smearor/nerd_fonts/icons/. Each icon follows the naming pattern
nf-{prefix}-{name}-symbolic (kebab-case, lower-case).
#![allow(unused)]
fn main() {
use nerd_fonts_rs::gtk::resolve_gtk_nerd_icon;
let icon_name = resolve_gtk_nerd_icon("nf-fa-gamepad");
// Returns "nf-fa-gamepad-symbolic"
}
Glyph Export
Glyphs are extracted as GTK4 symbolic SVG icons at build time using
skrifa for outline parsing. The export
logic lives in the fonts-rs-generator crate and is called automatically by
each font family crate’s build.rs.
Automatic Generation
build.rs generates the icon resources on demand via FontBuild. The export
is triggered when the font file hash has changed since the last export:
flowchart TD
A["build.rs runs"] --> B{"resources/.hash exists?"}
B -->|No| D["Export glyphs from font"]
B -->|Yes| C{"Font hash matches?"}
C -->|No| D
C -->|Yes| E["Skip export"]
D --> F["Write SVGs, metadata.json, icons.gresource.xml"]
F --> G["Write .hash"]
E --> H["Compile GResource bundles"]
G --> H
H --> I["Generate phf::Map + icon constants"]
resources/{font}.ttf
↓ build.rs (skrifa outline extraction, via ExportConfig)
resources/scalable/glyphs/*.svg (SVG files)
resources/metadata.json (name, codepoint, file path)
resources/icons.gresource.xml (GResource manifest)
resources/.hash (FNV-1a hash of font file)
↓ build.rs (code generation via FontBuild)
$OUT_DIR/codemap.rs (phf::Map<char, &str> + phf::Map<&str, char>)
$OUT_DIR/icons.rs (icon name constants)
$OUT_DIR/variant.rs (GRESOURCE_PREFIX + GLYPH_PREFIX)
↓ build.rs (glib-build-tools)
icons.gresource (icon GResource bundle)
The generated files (scalable/, metadata.json, icons.gresource.xml,
.hash) are in .gitignore and not checked into the repository. They are
regenerated from the font file as needed.
Caching
FontBuild uses two mechanisms for caching:
Cargo rebuild triggers (cargo:rerun-if-changed):
- The font file - triggers
build.rsre-execution resources/metadata.json- triggers phf::Map regenerationresources/icons.gresource.xml- triggers GResource recompilationbuild.rs- triggers full rebuild
Font hash comparison (FNV-1a):
When build.rs runs, it computes an FNV-1a hash of the font file and compares
it against the stored hash in resources/.hash. If the hashes differ (or the
file is missing), the full glyph export is triggered. Otherwise the export is
skipped.
For variable font variants, extra_hash() adds the variant name to the
hash, forcing re-export when the active variant changes even if the font file
is unchanged.
This means:
- Fresh clone: hash missing → export runs
- Font updated: hash differs → export runs automatically
- No changes: hash matches → export skipped (fast incremental build)
- Variant changed: extra hash differs → export runs
No manual rm -rf or cargo clean is needed when updating the font file.
Two Export Methods
ExportConfig::export_glyphs (struct method)
Used by most font family crates. Normalizes glyph names to
{glyph_name_prefix}-{kebab} and renders SVGs with the configured axis
location. Supports name_filter for custom glyph filtering.
#![allow(unused)]
fn main() {
let config = ExportConfig::<DotoConfig>::with_variant(*entry);
FontBuild::new(&font_path)
.extra_hash(entry.as_str())
.run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
.map_err(|e| miette::miette!("{e}"))?;
}
ExportConfig::export_glyphs_by_name_map (struct method)
Used by fonts without PostScript glyph names (e.g. SMuFL fonts with post
table version 3.0). Glyph names come from an external metadata file parsed
into a GlyphNameMap.
#![allow(unused)]
fn main() {
let name_map: GlyphNameMap = serde_json::from_str(&glyphnames_json)?;
let config = ExportConfig::<BravuraConfig>::new();
FontBuild::new(&font_path)
.run(|font_path, resources_dir| {
config.export_glyphs_by_name_map(font_path, resources_dir, &name_map)
})
.map_err(|e| miette::miette!("{e}"))?;
}
FontDefinition::export_glyphs (trait default method)
Used by font families with semantic PostScript glyph names (e.g. Barcode,
Seven-Segment). Uses Self::normalize_name for type-safe glyph names and
renders SVGs at the default axis location.
How It Works
Glyph Outline Extraction
The export uses skrifa to parse font outlines and convert Bezier curve
commands directly into SVG path data:
| OutlineBuilder method | SVG path command |
|---|---|
move_to(x, y) | M x y |
line_to(x, y) | L x y |
quad_to(x1, y1, x, y) | Q x1 y1 x y |
curve_to(x1, y1, x2, y2, x, y) | C x1 y1 x2 y2 x y |
close() | Z |
Both TrueType (glyf) and OpenType/CFF outlines are supported transparently
by skrifa.
Codepoint Resolution
The export builds a reverse character map (GlyphId → char) by probing
codepoints in the ranges defined by FontFamilyConfig::CODEPOINT_RANGES:
BMP_RANGE-U+0000–U+FFFF(default, most fonts)ASCII_PRINTABLE_RANGE-U+0020–U+007E(barcode, segment fonts)PUA_RANGE-U+E000–U+F8FF(icon fonts, SMuFL)SUPPLEMENTARY_PUA_RANGE-U+F0001–U+10FFFF(Nerd Fonts)
Name Normalization
Glyph names from the font’s post/CFF tables are normalized to
GTK-friendly icon names:
- Lowercase
- Replace
_with- - Replace non-alphanumeric characters (except
-) with- - Collapse consecutive
- - Strip leading/trailing
- - Prefix with
{glyph_name_prefix}-(e.g.doto-,dseg7-)
For Nerd Fonts, the prefix is nf- and the suffix -symbolic is added.
Empty Glyphs
Some glyphs (e.g. nonmarkingreturn, blank) have no outline data. These
are exported as minimal empty SVGs so their icon names remain registered in
metadata.json.
Crate Structure
The export logic lives in the fonts-rs-generator crate:
# Font family crate's Cargo.toml (build-dependency)
[build-dependencies]
fonts-rs-generator.workspace = true
fonts-rs-model.workspace = true
miette.workspace = true
See Build Patterns for complete build.rs examples.
Font Loading
The fonts module (enabled with the render feature) handles loading font
files for use with pixel-drawing
or other software rendering via ab_glyph.
impl_font_loader! Macro
Each font family crate uses the impl_font_loader! macro from
fonts-rs-generator to generate a cached font loading function:
#![allow(unused)]
fn main() {
// Literal: relative to resources/
fonts_rs_generator::impl_font_loader!("Doto.ttf");
// Env var: absolute path set by build.rs via cargo:rustc-env
fonts_rs_generator::impl_font_loader!(env: "DOTO_FONT_PATH");
}
The macro generates a font() -> Option<&'static ab_glyph::FontVec> function
that loads the TTF font file and caches it in a OnceLock. Subsequent calls
return a reference to the cached FontVec.
Loading Modes
Embedded Fonts (embed-fonts feature)
Font files are compiled into the binary via include_bytes!:
#![allow(unused)]
fn main() {
// No runtime file access needed
fonts_rs_doto::fonts::font(); // Returns Option<&'static FontVec>
}
From Disk (default)
Fonts are loaded from a path set by build.rs via cargo:rustc-env.
The impl_font_loader!(env: ...) variant reads the path from the
environment variable at runtime.
Caching
Fonts are loaded once and cached in a OnceLock. Subsequent calls return
a reference to the cached FontVec:
#![allow(unused)]
fn main() {
use fonts_rs_doto::fonts::font;
let font1 = font().unwrap();
let font2 = font().unwrap();
assert!(core::ptr::eq(font1 as *const _, font2 as *const _));
}
Nerd Fonts (nerd-fonts-rs)
The nerd-fonts-rs crate provides additional font loading for Nerd Fonts
specifically:
- Nerd Font (
SymbolsNerdFont-Regular.ttf) - TTF file for rendering Nerd Font icons - Label Font (
JetBrainsMonoNLNerdFont-Regular.woff2) - WOFF2 file decompressed to TTF at load time for text labels
The label font is stored as WOFF2. The convert_woff2 function decompresses
it to TTF using the woff2-patched crate. If decompression fails, it falls
back to parsing the data directly as TTF.
Initialization
For nerd-fonts-rs, call init() once at application startup:
#![allow(unused)]
fn main() {
// Use default base directory (relative to executable)
nerd_fonts_rs::init(None);
// Or specify a custom base directory
nerd_fonts_rs::init(Some("/usr/share/fonts/nerd-fonts"));
}
CSS Generation
The css module provides CSS string constants for GTK and web usage.
GTK Font-Face CSS
The font_face_css() function generates a version-adapted @font-face CSS
string at runtime. It detects the GTK4 library version and adjusts the src
descriptor syntax for compatibility:
- GTK >= 4.10: emits
src: url("...") format("truetype")for better spec compliance with the new GtkCssParser. - GTK < 4.10: emits
src: url("...")without aformat()hint to avoid parse errors with the legacy parser.
It is loaded automatically by init() when the gtk feature is enabled.
#![allow(unused)]
fn main() {
use nerd_fonts_rs::css::font_face_css;
let css = font_face_css();
// The CSS references GResource URLs:
// resource:///io/smearor/nerd_fonts/SymbolsNerdFont-Regular.ttf
// resource:///io/smearor/nerd_fonts/SymbolsNerdFontMono-Regular.ttf
}
The CSS defines:
@font-faceforNerdFontsSymbolsOnly(proportional)@font-faceforNerdFontsSymbolsOnlyMono(monospace).nerd-iconclass for proportional icons.nerd-icon-monoclass for monospace icons
The static FONT_FACE_CSS constant retains the baseline CSS (without
format() hints) for backward compatibility and testing.
Runtime Version Detection
The GtkVersion struct wraps the runtime GTK4 version and provides
comparison helpers:
#![allow(unused)]
fn main() {
use nerd_fonts_rs::css::GtkVersion;
#[cfg(feature = "gtk")]
let version = GtkVersion::runtime();
let legacy = GtkVersion::new(4, 8, 0);
assert!(!legacy.at_least(4, 10));
let modern = GtkVersion::new(4, 14, 2);
assert!(modern.at_least(4, 10));
}
GResource Prefix
The GRESOURCE_PREFIX constant defines where font files are registered in the
GResource system:
#![allow(unused)]
fn main() {
use nerd_fonts_rs::css::GRESOURCE_PREFIX;
assert_eq!(GRESOURCE_PREFIX, "/io/smearor/nerd_fonts");
}
Manual CSS Loading
If you need to load the CSS manually (e.g., in a custom initialization flow):
#![allow(unused)]
fn main() {
use gtk4::CssProvider;
let provider = gtk4::CssProvider::new();
let css = nerd_fonts_rs::css::font_face_css();
#[cfg(feature = "v4_12")]
provider.load_from_string(&css);
#[cfg(not(feature = "v4_12"))]
{
#[allow(deprecated)]
provider.load_from_data(&css);
}
if let Some(display) = gtk4::gdk::Display::default() {
gtk4::style_context_add_provider_for_display(
&display,
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
GTK Integration
Font Family Crates
Each font family crate provides a register_glyphs() function (enabled with
the gtk feature) that registers the compiled GResource bundle:
use fonts_rs_doto::register_glyphs;
fn main() {
// Register GResource (call once at startup)
register_glyphs().unwrap();
// Now GTK can resolve icon names from the GResource bundle
let image = gtk4::Image::from_icon_name("doto-a");
}
register_glyphs() calls gio::resources_register_include!("icons.gresource")
to register the compiled GResource binary containing all SVG glyph files.
Nerd Fonts (nerd-fonts-rs)
The nerd-fonts-rs crate provides additional GTK4 functionality:
Initialization
Call init() once at application startup:
use nerd_fonts_rs::init;
fn main() {
init(None);
// ...
}
init() performs the following:
- Font initialization (with
renderfeature) - Sets the font search base directory - GResource registration - Registers the compiled GResource binary
- Icon registration - Registers vendored Nerd Font SVG icons
- CSS loading - Loads version-adapted
@font-faceCSS viafont_face_css()into aCssProviderand adds it to the default display
GTK Icon Name Resolution
#![allow(unused)]
fn main() {
use nerd_fonts_rs::gtk::resolve_gtk_nerd_icon;
let icon_name = resolve_gtk_nerd_icon("nf-fa-gamepad");
// Returns "nf-fa-gamepad-symbolic"
let image = gtk4::Image::from_icon_name(&icon_name.unwrap());
}
The function normalizes CSS class names (like nf-fa-gamepad or fa-gamepad)
into GTK icon names that gtk4::Image::from_icon_name understands.
Web CSS
The web module (enabled with the web feature) provides a CSS constant for
web-based widget rendering.
WEB_NERDFONT_CSS
The WEB_NERDFONT_CSS constant contains a complete CSS stylesheet with:
@font-facerules for loading Nerd Font symbol fonts- Per-icon
content: "\XXXX"mappings for use in web widgets
#![allow(unused)]
fn main() {
use nerd_fonts_rs::web::WEB_NERDFONT_CSS;
// Include in your web HTML
let html = format!(
"<style>{}</style><span class='nf-fa-gamepad'></span>",
WEB_NERDFONT_CSS
);
}
Usage with Web Instances
The web CSS is designed for web-based widget rendering systems that display
Nerd Font icons in HTML pages. Each icon class maps to its Unicode codepoint
via the CSS content property.
Example HTML
<link rel="stylesheet" href="nerdfont.css">
<span class="nerd-icon nf-fa-gamepad"></span>
The CSS is generated from the SymbolsNerdFont-Regular.ttf glyph table and
includes mappings for all Nerd Font symbols.
Platform Notes
Linux
Linux is the primary target for the gtk feature.
System Dependencies
# Ubuntu/Debian
sudo apt-get install -y pkg-config libgtk-4-dev libglib2.0-dev
# Fedora
sudo dnf install -y pkg-config gtk4-devel glib2-devel
# Arch Linux
sudo pacman -S pkgconf gtk4 glib2
GResource Compilation
The build.rs script uses glib_build_tools::compile_resources to compile
the GResource XML into a binary. This requires glib-build-tools as a
build dependency and the GTK 4 development headers to be installed.
macOS
The gtk feature can be used on macOS with GTK 4 installed via Homebrew:
brew install gtk4
No additional configuration is needed beyond the standard Rust build process.
Windows
The gtk feature can be used on Windows with GTK 4 installed via MSYS2 or
gvsbuild. See the GTK4 Windows installation
guide for details.
Font Loading Mode
The render feature enables font loading (TTF/WOFF2) via ab_glyph. It has no
platform-specific dependencies and works on any platform supported by Rust.
[dependencies]
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["render"] }
Web-Only Mode
The web feature has no platform-specific dependencies. It simply provides a
CSS string constant.
[dependencies]
nerd-fonts-rs = { version = "0.1", default-features = false, features = ["web"] }
Embedded Fonts
The embed-fonts feature compiles font files directly into the binary. This
increases binary size by approximately 200-500 KB but eliminates the need for
runtime file access. This is useful for:
- Standalone binaries
- Environments without a filesystem
- Simplified deployment