Skip to content

Font Configuration

Vulpis features a robust font registry system designed to decouple UI logic from file paths. It supports automatic discovery, complex family relationships (aliasing), and runtime dynamic loading.

1. Automatic Font Discovery

On engine startup, Vulpis scans the assets/ directory recursively. Any valid font file found is automatically registered into the global registry.

  • Logic: The engine looks for files ending in .ttf or .otf (case-insensitive).
  • Alias Generation: The filename without extension becomes the font alias.
  • Defaults: Auto-loaded fonts are registered with a default size of 16px and fallback = false.

Example: If you have assets/fonts/ui/Roboto-Bold.ttf:

  1. The engine finds it.
  2. It registers the alias "Roboto-Bold".
  3. You can use it immediately: el.Text({ style = { fontFamily = "Roboto-Bold" } }).

2. Global Configuration (VP_FONT_CONFIG.lua)

For more control (defining variants, setting default sizes, or creating fallbacks), use the config/VP_FONT_CONFIG.lua file.

Note: Configuration defined here overrides automatically discovered fonts. If an auto-loaded font has the same name (alias) as an entry here, the settings in this file take precedence

A. Basic Registration

Define a font by providing its file path relative to the assets/ folder.

lua
return {
    -- "body" is the alias used in your UI components
    body = {
        path = "fonts/Inter-Regular.ttf",
        size = 14  -- Default size if not overridden in style
    },
    
    -- "header"
    header = {
        path = "fonts/Montserrat-Bold.ttf",
        size = 32
    }
}

B. Font Variants (Styles & Weights)

Vulpis allows you to map specific styles (like fontWeight="bold") to specific font files. This is handled via the variants table within a font configuration.

Supported Variant Keys: The engine's getVariantKey function recognizes these exact keys:

  • "bold"
  • "italics"
  • "bold-italics"
  • "semi-bold"
  • "thin"

Defining Variants: You can define a variant using a table (for path/size) or a string (for simple path/alias).

lua
return {
    sans = {
        path = "fonts/OpenSans-Regular.ttf",
        size = 16,
        
        variants = {
            -- Method 1: Explicit Path
            ["bold"] = { path = "fonts/OpenSans-Bold.ttf" },
            
            -- Method 2: String Path (Shorthand)
            ["italics"] = "fonts/OpenSans-Italic.ttf",
            
            -- Method 3: Pointing to another Alias (See Section C below)
            ["thin"] = "open_sans_light" 
        }
    },
    
    -- Define the alias referenced above
    open_sans_light = { path = "fonts/OpenSans-Light.ttf" }
}

C. Aliasing & Redirection

You can define a font family that is simply a pointer to another existing family using alias_of. This is useful for semantic naming (e.g., mapping "ui_main" to "inter").

Top-Level Aliasing:

lua
return {
    inter = { path = "fonts/Inter-Regular.ttf" },
    
    -- "ui_text" inherits path and variants from "inter"
    ui_text = { alias_of = "inter", size = 18 } 
}

Variant Aliasing: A powerful feature is using aliases inside variant definitions. If a variant's value is a string, the engine checks if that string is a registered alias before treating it as a file path.

lua
return {
    -- 1. Register the bold font as a standalone family
    my_bold_font = { path = "fonts/MyFont-Bold.ttf" },

    -- 2. Use that family as the "bold" variant for the main family
    main = {
        path = "fonts/MyFont-Regular.ttf",
        variants = {
            -- The engine sees "my_bold_font" is a registered key, 
            -- so it resolves to "fonts/MyFont-Bold.ttf"
            ["bold"] = "my_bold_font" 
        }
    }
}

D. Fallbacks

When a font lacks a specific character (glyph), the engine looks through a global list of fallback fonts.

To add a font to this list, set fallback = true.

lua
return {
    -- Main font (Latin characters)
    default = { path = "fonts/PixelOperator.ttf" },

    -- Fallback for Emoji (added to fallback list)
    emoji = { 
        path = "fonts/NotoColorEmoji.ttf", 
        fallback = true 
    },

    -- Fallback for Japanese (added to fallback list)
    jp = { 
        path = "fonts/NotoSansJP.ttf", 
        fallback = true 
    }
}

Note: The engine rebuilds the fallback list whenever the configuration changes.

3. Engine Configuration (VP_ENGINE_CONFIG.lua)

This file resides in the config/ folder and controls global engine settings.

enable_default_fonts

  • Type: boolean
  • Default: true

By default, Vulpis loads an internal "Noto Sans" font and registers it as "default" if no other "default" family is defined.

lua
-- config/VP_ENGINE_CONFIG.lua
enable_default_fonts = false

Warning: If you set this to false, you must register a family named "default" in VP_FONT_CONFIG.lua. If a text node does not specify a fontFamily and no "default" exists, it will fail to render.

4. Runtime API (Lua)

Vulpis exposes functions to manage fonts dynamically during gameplay/app execution.

update_font_config(alias, config_table)

Updates the registry at runtime. This triggers a rebuild of the fallback list.

Parameters:

  • alias (string): The family name.
  • config_table (table): Same structure as entries in VP_FONT_CONFIG.lua (path, size, fallback).
lua
-- Example: Allow user to load a custom font file
update_font_config("user_custom", {
    path = "user/themes/dark/font.ttf",
    size = 20
})

-- Now usable in components
el.Text({ style = { fontFamily = "user_custom" } })

load_font(path, size, [style_flags])

The load_font function allows you to bypass the global registry and load a specific font file directly from your assets. It returns a Font Handle (userdata) that can be passed directly to a component's style.

Function Signature:

lua
local handle  = load_font(path, size)

Parameters:

ParameterTypeRequiredDefaultDescription
pathstringYes-The file path relative to the assets/ directory (e.g., "fonts/MyFont.ttf").
sizenumberNo16The font size in pixels.
style_flagstableNonilA table of flags to apply algorithmic styling (see below).

Style Flags: If you lack a specific font variant (like a bold file), you can force the engine to generate it algorithmically.

KeyTypeDescription
boldbooleanApplies a thickening filter to the glyphs (Fake Bold).
italicbooleanApplies a slant transformation to the glyphs (Fake Italic).
thinbooleanApplies a thinning filter to the glyphs.

Returns:

  • Success: Returns a FontHandle userdata object.
  • Failure: Returns nil followed by an error string.

Usage Examples

1. Basic Loading

Load a font at a specific size.

lua
local titleFont = load_font("fonts/Inter-Regular.ttf", 32)

2. Synthetic Styling (Fake Bold/Italic)

Load a regular font file but make it look bold and italic using engine algorithms.

lua
local mathFont = load_font("fonts/FiraCode.ttf", 20, { 
    bold = true, 
    italic = true 
})

4. Applying to Components

To use the handle, assign it to the font property in your component's style.

Note: The font property takes precedence over fontFamily. If both are provided, fontFamily is ignored.

lua
local customHandle = load_font("fonts/Retro.ttf", 40)

el.Text({
    text = "Direct Handle Usage",
    style = {
        font = customHandle, -- Pass the handle here
        color = "#FF00FF"
    }
})

5. Usage in Components

The fontFamily Property

This is the standard way to apply fonts. The engine resolves the alias to a file path, checks for variants based on fontWeight/fontStyle, and loads the resource.

lua
el.Text({
    text = "Detailed Example",
    style = {
        fontFamily = "sans",  -- Looks up "sans" in registry
        fontWeight = "bold",  -- Looks for ["bold"] variant in "sans" config
        fontStyle = "italics" -- If "bold-italics" variant exists, uses that
    }
})

The font Property (Raw Handle)

For advanced use cases (like preloading), you can pass a userdata handle directly. **This takes precedence over fontFamily**.

lua
-- Preload once
local cachedFont = load_font("assets/fonts/Fixed.ttf", 12)

el.Text({
    text = "Performance Critical Text",
    style = {
        font = cachedFont, -- Skips registry lookup
        color = "#FFFFFF"
    }
})