Initial work on the system font resolver.

This commit is contained in:
Martín Lucas Golini
2026-05-13 17:32:39 -03:00
parent 4b58b2f61b
commit 828adf581d
22 changed files with 2698 additions and 36 deletions

View File

@@ -0,0 +1,710 @@
# SystemFontResolver Architecture Plan
## Problem Statement
`eepp` has no mechanism to discover fonts installed on the host operating system. When CSS specifies `font-family: "Helvetica Neue", Arial, sans-serif`, the current resolution chain (`UISceneNode::getFontFromNamesList``FontManager::getByName`) can only find fonts that were explicitly loaded via `@font-face` or programmatic `FontTrueType::New`. System font keywords (`caption`, `icon`, `menu`, etc.) are parsed but silently discarded. This makes cross-platform CSS font-family resolution impossible.
**Goal:** Implement a cross-platform `SystemFontResolver` abstraction that maps CSS `font-family` + weight/style properties to physical font file paths (and face indices for `.ttc` files) by querying the host OS font subsystem.
---
## 1. Target Platform APIs
| Platform | EE_PLATFORM | OS API | Headers/Libraries |
|----------|-------------|--------|-------------------|
| Windows | `EE_PLATFORM_WIN` | DirectWrite | `<dwrite.h>`, `dwrite.lib` |
| macOS | `EE_PLATFORM_MACOS` | Core Text | `<CoreText/CoreText.h>`, `-framework CoreText` |
| iOS | `EE_PLATFORM_IOS` | Core Text | same as macOS |
| Linux | `EE_PLATFORM_LINUX` | Fontconfig | `<fontconfig/fontconfig.h>`, `-lfontconfig` |
| FreeBSD | `EE_PLATFORM_BSD` | Fontconfig | same as Linux |
| Android (API 29+) | `EE_PLATFORM_ANDROID` | NDK Font API | `<android/font.h>`, `<android/font_matcher.h>`, `-landroid` |
| Android (legacy) | `EE_PLATFORM_ANDROID` | XML fallback | `/system/etc/fonts.xml` |
| Haiku | `EE_PLATFORM_HAIKU` | Interface Kit / BFont | `<Font.h>`, `-lbe` |
---
## 2. Interface Definition
### 2.1 Header: `include/eepp/graphics/systemfontresolver.hpp`
```cpp
#ifndef EE_GRAPHICS_SYSTEMFONTRESOLVER_HPP
#define EE_GRAPHICS_SYSTEMFONTRESOLVER_HPP
#include <eepp/config.hpp>
#include <eepp/system/singleton.hpp>
#include <eepp/graphics/base.hpp>
#include <string>
#include <vector>
#include <unordered_map>
#include <cstdint>
namespace EE { namespace Graphics {
/** Weight scale matching CSS font-weight (100-900 + keywords mapped to numeric) */
enum class FontWeight : Uint16 {
Thin = 100,
ExtraLight = 200,
Light = 300,
Normal = 400,
Medium = 500,
SemiBold = 600,
Bold = 700,
ExtraBold = 800,
Black = 900
};
/** Categorization of font stretch/width */
enum class FontStretch : Uint8 {
UltraCondensed = 1,
ExtraCondensed = 2,
Condensed = 3,
SemiCondensed = 4,
Normal = 5,
SemiExpanded = 6,
Expanded = 7,
ExtraExpanded = 8,
UltraExpanded = 9
};
/** Query sent to the OS font subsystem */
struct FontQuery {
std::string family; ///< CSS font-family string (e.g. "Arial", "sans-serif")
FontWeight weight { FontWeight::Normal };
FontStretch stretch { FontStretch::Normal };
bool italic { false };
};
/** Resolved physical font from the OS */
struct FontDesc {
std::string path; ///< Full filesystem path to the font file
Uint32 faceIndex {0}; ///< Face index for .ttc/.otc TrueType Collections
FontWeight weight { FontWeight::Normal };
FontStretch stretch { FontStretch::Normal };
bool italic { false };
bool monospace { false };
};
/** Generic font family classification */
enum class GenericFamily : Uint8 {
Serif,
SansSerif,
Monospace,
Cursive,
Fantasy,
SystemUi, ///< OS default UI font
Emoji,
Unknown
};
/** @brief Cross-platform system font discovery and resolution.
*
* Queries the host OS font subsystem to enumerate installed fonts and resolve
* CSS font-family names to physical file paths. All results are cached to
* avoid repeated OS API calls. This is NOT a Font subclass — it is a
* pure resolver that returns file paths consumable by FontTrueType.
*/
class EE_API SystemFontResolver {
SINGLETON_DECLARE_HEADERS( SystemFontResolver )
public:
~SystemFontResolver();
/** Enumerate all fonts installed on the system. Expensive — call once, results cached. */
const std::vector<FontDesc>& enumerate();
/** Enumerate fonts matching a specific family name. */
std::vector<FontDesc> enumerateFamily( const std::string& family );
/** Find the best matching font file for a given query.
* Searches by family, then falls back to generic family defaults. */
FontDesc resolve( const FontQuery& query );
/** Resolve with a font-family CSS list string (e.g. "Helvetica Neue, Arial, sans-serif").
* Returns the best match across the entire list. */
FontDesc resolveFromNamesList( const std::string& namesList, FontWeight weight, bool italic );
/** Map a generic CSS family keyword to the OS default font path. */
FontDesc resolveGeneric( GenericFamily generic, FontWeight weight, bool italic );
/** Get the default system UI font path. */
FontDesc getSystemFont() const;
/** Get the default monospace font path. */
FontDesc getSystemMonospaceFont() const;
/** Find a fallback font that contains a specific Unicode codepoint. */
FontDesc getFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic );
/** Check if a font file contains a specific codepoint. */
bool fontContainsCodepoint( const std::string& path, Uint32 codepoint,
Uint32 faceIndex = 0 );
/** Clear the internal caches. Call when fonts are installed/uninstalled at runtime. */
void invalidateCache();
protected:
SystemFontResolver();
private:
/** Populate the full font list from the OS. Platform-specific. */
void populateFontList();
/** Map a generic family to platform-default font paths. */
void populateGenericFallbacks();
/** The raw enumerated list from the OS. Populated once, never cleared. */
std::vector<FontDesc> mFontList;
bool mFontListPopulated{ false };
/** Cache: (family_lower, weight, stretch, italic) → FontDesc.
* Key uses the lower-cased family name to make lookups case-insensitive. */
struct CacheKey {
String::HashType familyHash;
Uint16 weight;
Uint8 stretch;
bool italic;
bool operator==( const CacheKey& o ) const {
return familyHash == o.familyHash && weight == o.weight &&
stretch == o.stretch && italic == o.italic;
}
};
struct CacheKeyHasher {
Uint64 operator()( const CacheKey& k ) const {
return ( static_cast<Uint64>( k.familyHash ) << 32 ) |
( static_cast<Uint64>( k.weight ) << 16 ) |
( static_cast<Uint64>( k.stretch ) << 8 ) |
( static_cast<Uint64>( k.italic ) );
}
};
mutable UnorderedMap<CacheKey, FontDesc, CacheKeyHasher> mResolveCache;
/** Cache: generic + weight + italic → FontDesc */
mutable UnorderedMap<Uint32, FontDesc> mGenericCache;
/** Cache for codepoint fallback lookups. */
mutable UnorderedMap<Uint32, std::string> mCodepointFallbackCache;
/** Pre-computed generic family mappings (e.g. "sans-serif" → platform default). */
struct GenericEntry {
GenericFamily generic;
FontDesc desc;
};
std::vector<GenericEntry> mGenericFallbacks;
/** Best-match scoring: given a query and a candidate, compute a fit score (lower is better). */
static int scoreMatch( const FontQuery& query, const FontDesc& candidate );
};
}} // namespace EE::Graphics
#endif
```
### 2.2 Key Design Decisions
**Singleton Pattern.** Uses the existing `SINGLETON_DECLARE_HEADERS` / `SINGLETON_DECLARE_IMPLEMENTATION` macros (same as `FontManager`). The singleton is created on first `instance()` call.
**Not a Font subclass.** The resolver returns `FontDesc` structs (file paths), not `Font*` objects. This avoids coupling font discovery to the FreeType/GPU resource lifecycle. The consumer (`FontManager` or `UISceneNode`) is responsible for creating `FontTrueType` instances from the paths.
**Case-insensitive matching.** CSS font-family names are case-insensitive. Internal lookups use `String::toLower()` on family names before hashing/querying.
**faceIndex support.** TrueType Collections (`.ttc`, `.otc`) contain multiple faces in a single file. The `faceIndex` field in `FontDesc` tells `FontTrueType` which face to load from the file.
---
## 3. Factory Routing (Platform Selection)
### 3.1 Source File Organization
```
src/eepp/graphics/
├── systemfontresolver.hpp # Public header
├── systemfontresolver.cpp # Common implementation (cache logic, matching, generic fallbacks)
├── systemfontresolver_win.cpp # Windows / DirectWrite
├── systemfontresolver_macos.cpp # macOS+iOS / Core Text
├── systemfontresolver_linux.cpp # Linux+BSD / Fontconfig
├── systemfontresolver_android.cpp # Android / NDK Font API + XML fallback
└── systemfontresolver_haiku.cpp # Haiku / BFont
```
### 3.2 Routing in `systemfontresolver.hpp`
```cpp
#if EE_PLATFORM == EE_PLATFORM_WIN
#define EE_SYSTEMFONT_PLATFORM_DEFINED
#elif EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_IOS
#define EE_SYSTEMFONT_PLATFORM_DEFINED
#elif EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD
#define EE_SYSTEMFONT_PLATFORM_DEFINED
#elif EE_PLATFORM == EE_PLATFORM_ANDROID
#define EE_SYSTEMFONT_PLATFORM_DEFINED
#elif EE_PLATFORM == EE_PLATFORM_HAIKU
#define EE_SYSTEMFONT_PLATFORM_DEFINED
#elif EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
// Emscripten: no system font access; use baked-in fonts only
#endif
```
### 3.3 `systemfontresolver.cpp` Common Implementation
Houses:
- `populateFontList()` stubs (where each platform's implementation gets compiled in)
- `resolve()` — the main matching algorithm: lower-case lookup in `mResolveCache`, iterate `mFontList`, compute `scoreMatch()` scores, return best.
- `resolveGeneric()` — look up `mGenericCache`, fall back to `isFamilyInList()` search, else use hard-coded platform defaults.
- `resolveFromNamesList()` — split by comma, try each family name, `resolve()` each.
- `getFallbackForCodepoint()` — iterate font list, call `fontContainsCodepoint()`, cache results.
- `fontContainsCodepoint()` — use FreeType `FT_New_Face(..., faceIndex, ...)`, `FT_Get_Char_Index()`, then `FT_Done_Face()`.
- `scoreMatch()` — weighted scoring: exact family match (0), family substring match (10), generic match (50), weight distance, italic mismatch penalty, stretch distance.
### 3.4 Platform Implementations (in separate .cpp files)
Each platform file implements only `SystemFontResolver::populateFontList()` and `SystemFontResolver::getSystemFont()`. Common code stays in `systemfontresolver.cpp`.
| Platform | populateFontList Implementation |
|----------|-------------------------------|
| Windows | `IDWriteFactory::GetSystemFontCollection()` → enumerate `IDWriteFontFamily``IDWriteFont``IDWriteLocalizedStrings` for family name → `IDWriteFontFace::GetFiles()` for paths |
| macOS/iOS | `CTFontManagerCopyAvailableFontFamilyNames()` → for each family: `CTFontDescriptorCreateMatchingFontDescriptors()``CTFontDescriptorCopyAttribute(kCTFontURLAttribute)` |
| Linux/BSD | `FcInit()``FcPatternCreate()``FcFontList()``FcPatternGetString(FC_FILE)`, `FcPatternGetInteger(FC_INDEX)`, `FcPatternGetInteger(FC_WEIGHT)`, `FcPatternGetInteger(FC_SLANT)` |
| Android (29+) | `AFontMatcher_create()``AFontMatcher_setFamilyVariant()` → use `AFontMatcher` to enumerate |
| Android (legacy) | Parse `/system/etc/fonts.xml` for `<family>``<font>` elements |
| Haiku | `BFont::get_family_and_style()` + iterate `BPath` for font directories |
---
## 4. Memory & Performance Strategy
### 4.1 Caching Architecture (Two Layers)
```
┌──────────────────────────────────────────────────────────┐
│ Layer 1: Enumerated Font List (mFontList) │
│ - Populated ONCE per process lifetime │
│ - std::vector<FontDesc>, pre-sorted by family │
│ - Populated lazily on first enumerate()/resolve() call │
│ - Thread-safe: populated under mutex, read-only after │
│ - Memory: ~2-5KB per 1000 system fonts (path strings) │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Layer 2: Resolution Cache (mResolveCache) │
│ - Key: (familyHash, weight, stretch, italic) │
│ - Value: FontDesc │
│ - Built lazily: first resolve(family, weight) calls │
│ - Hit rate: ~100% after warm-up (font queries are few) │
│ - Thread-safe: lockless reads after population │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Layer 2b: Codepoint Fallback Cache │
│ - Key: Uint32 codepoint │
│ - Value: font file path string │
│ - Built lazily per codepoint │
│ - Hit rate: ~100% for commonly-missing CJK/emoji chars │
└──────────────────────────────────────────────────────────┘
```
### 4.2 Performance Characteristics
| Operation | Cold Path | Hot Path |
|-----------|-----------|----------|
| `enumerate()` | 10-80ms (OS query + path normalization) | <1us (cached) |
| `resolve(family, weight)` | First call per family: enumerates all fonts if not yet done | <2us (hash lookup + array scan of family matches) |
| `resolveFromNamesList("Arial, sans-serif", ...)` | Same as resolve for first family | <10us (try each family in list, first hit returns) |
| `getFallbackForCodepoint(0x65E5)` | ~5ms (FreeType FT_New_Face + FT_Get_Char_Index + FT_Done_Face, may scan multiple fonts) | <1us (cached string + path) |
| `fontContainsCodepoint()` | ~1-3ms per font file (FT_New_Face, FT_Get_Char_Index, FT_Done_Face) | Result not cached separately (only via getFallbackForCodepoint) |
### 4.3 Render-Loop Safety
**Critical invariant:** `SystemFontResolver` is NEVER called during the render loop. It is called only during:
1. Style resolution (when CSS `font-family` is applied to a widget this happens during layout/initialization, not drawing)
2. `UISceneNode::getFontFromNamesList()` called during `applyProperty()` which runs on the UI thread but outside the draw cycle
3. Font fallback codepoint lookups triggered by `FontTrueType::getGlyph()`, which calls `eeASSERT( Engine::isMainThread() )` and runs on the main thread but MUST NOT call the OS. See Section 6 for the async fallback strategy.
**For fallback codepoint resolution**, the OS query is too slow for the render path. Instead:
- `getFallbackForCodepoint()` with cache hit O(1) return, safe for render
- `getFallbackForCodepoint()` with cache miss queues a deferred load, returns "not found" to render thread, triggers a callback once resolved
### 4.4 Memory Footprint Strategy
- **`mFontList`** (vector of FontDesc): Each entry is ~80 bytes (path string SSO + metadata). For 1000 fonts: ~80KB. Acceptable.
- **`mResolveCache`** (hash map): Each entry is ~100 bytes. For 100 resolved queries: ~10KB.
- **`mCodepointFallbackCache`**: Each entry is ~80 bytes. For 1000 codepoints: ~80KB.
- **Total budget:** Under 200KB. No heap pressure beyond this.
---
## 5. FreeType Integration (faceIndex + Loading)
### 5.1 Changes to `FontTrueType::loadFromFile`
**Current** (fonttruetype.cpp:350):
```cpp
if ( FT_New_Face( static_cast<FT_Library>( mLibrary ), filename.c_str(), 0, &face ) != 0 ) {
```
**Modified** add default parameter `faceIndex = 0`:
```cpp
bool loadFromFile( const std::string& filename, Uint32 faceIndex = 0 );
```
Internal change use the parameter:
```cpp
if ( FT_New_Face( static_cast<FT_Library>( mLibrary ), filename.c_str(),
static_cast<FT_Long>( faceIndex ), &face ) != 0 ) {
```
### 5.2 Changes to `FontTrueType::loadFromMemory`
**Current** (fonttruetype.cpp:384-385):
```cpp
if ( FT_New_Memory_Face( static_cast<FT_Library>( mLibrary ),
reinterpret_cast<const FT_Byte*>( ptr ),
static_cast<FT_Long>( sizeInBytes ), 0, &face ) != 0 ) {
```
**Modified** add `faceIndex` parameter:
```cpp
bool loadFromMemory( const void* data, std::size_t sizeInBytes, bool copyData = true,
Uint32 faceIndex = 0 );
```
### 5.3 Changes to `FontTrueType::loadFromStream`
**Current** (fonttruetype.cpp:427):
```cpp
if ( FT_Open_Face( static_cast<FT_Library>( mLibrary ), &args, 0, &face ) != 0 ) {
```
**Modified** add `faceIndex` parameter:
```cpp
bool loadFromStream( IOStream& stream, Uint32 faceIndex = 0 );
```
### 5.4 Changes to `FontTrueType::loadFromPack`
Add `faceIndex` passthrough:
```cpp
bool loadFromPack( Pack* pack, std::string filePackPath, Uint32 faceIndex = 0 );
```
### 5.5 Changes to `FontTrueType` Static Factory
Add a new constructor variant:
```cpp
static FontTrueType* New( const std::string& FontName, const std::string& filename,
Uint32 faceIndex );
```
### 5.6 New Member: `mFaceIndex`
```cpp
// In fonttruetype.hpp, protected section:
Uint32 mFaceIndex{ 0 }; ///< Face index for .ttc TrueType Collections
```
Stored on construction and passed through to FreeType calls. Since all current callers use `faceIndex = 0`, this is a purely additive, non-breaking change.
---
## 6. Fallback Glyph Routing Strategy
### 6.1 Current Fallback Chain in `FontTrueType::getGlyph`
The existing fallback chain (fonttruetype.cpp:549-619) is:
1. Emoji fallback (color emoji font emoji font)
2. BoldItalic variant
3. Bold variant
4. Italic variant
5. Own glyph index lookup
6. FontManager fallback fonts (if `mEnableFallbackFont`)
### 6.2 Proposed: Add OS-Level Fallback as Step 7
After step 6 fails (no fallback font from FontManager contains the glyph), add:
```cpp
// Step 7: Ask the OS for a font containing this codepoint
if ( 0 == idx && mEnableFallbackFont && mEnableSystemFallback ) {
FontDesc fallbackDesc = SystemFontResolver::instance()->getFallbackForCodepoint(
codePoint, FontWeight::Normal, false );
if ( !fallbackDesc.path.empty() ) {
FontTrueType* systemFallback = getOrLoadSystemFallbackFont( fallbackDesc );
if ( systemFallback && ( idx = systemFallback->getGlyphIndex( codePoint ) ) ) {
if ( mIsMonospace && mEnableDynamicMonospace ) {
mIsMonospaceComplete = false;
mUsingFallback = true;
}
return systemFallback->getGlyphByIndex( idx, characterSize, bold, italic,
outlineThickness, getPage( characterSize ) );
}
}
}
```
### 6.3 New Member: `mEnableSystemFallback`
```cpp
// In fonttruetype.hpp:
bool mEnableSystemFallback{ true };
public:
bool isSystemFallbackEnabled() const;
void setEnableSystemFallback( bool enableSystemFallback );
```
### 6.4 System Fallback Font Cache in FontManager
To avoid repeatedly loading the same system font file for every codepoint miss, `FontManager` maintains a small LRU cache of loaded system fallback fonts:
```cpp
// In fontmanager.hpp, protected section:
std::vector<std::unique_ptr<FontTrueType>> mSystemFallbackFonts;
static constexpr Uint32 MAX_SYSTEM_FALLBACK_FONTS = 8;
public:
FontTrueType* getOrLoadSystemFallbackFont( const FontDesc& desc );
```
**Flow:**
1. `FontManager::getOrLoadSystemFallbackFont()` checks if a `FontTrueType*` for the path+faceIndex already exists in `mSystemFallbackFonts`.
2. If yes, returns cached pointer.
3. If no, calls `FontTrueType::New(family, desc.path, desc.faceIndex)`, adds to cache. If cache exceeds `MAX_SYSTEM_FALLBACK_FONTS`, evicts LRU.
### 6.5 Asynchronous Codepoint Fallback Prefetch
For codepoints that the `getFallbackForCodepoint()` cache misses on, we have two options:
**Option A (Recommended): Synchronous with fast path.** On first miss for a codepoint, `getFallbackForCodepoint()` scans the already-enumerated `mFontList` in-memory (no OS calls), checks `fontContainsCodepoint()` with FreeType's `FT_New_Face` + `FT_Get_Char_Index` + `FT_Done_Face`. Since `FT_New_Face` for a single font file is ~0.5-2ms and we may need to scan ~20 fonts to find a match, total cost for first miss is ~10-40ms. This is acceptable because:
- It only happens once per unique missing codepoint (result is cached)
- It happens outside the render loop (during layout/initialization)
- For CJK text, common hanzi/kanji will be in the first few system CJK fonts
**Option B (Future optimization): Deferred.** On cache miss, enqueue a background task, return empty. Font will appear on next frame after load completes.
**We implement Option A for initial release.**
### 6.6 `fontContainsCodepoint()` Implementation Strategy
```cpp
bool SystemFontResolver::fontContainsCodepoint( const std::string& path, Uint32 codepoint,
Uint32 faceIndex ) {
// Use FreeType to quickly check without creating a full FontTrueType
FT_Library ftLib;
if ( FT_Init_FreeType( &ftLib ) != 0 )
return false;
FT_Face face;
if ( FT_New_Face( ftLib, path.c_str(), static_cast<FT_Long>( faceIndex ), &face ) != 0 ) {
FT_Done_FreeType( ftLib );
return false;
}
bool hasGlyph = FT_Get_Char_Index( face, codepoint ) != 0;
FT_Done_Face( face );
FT_Done_FreeType( ftLib );
return hasGlyph;
}
```
**Optimization:** Use a per-thread FreeType library to avoid `FT_Init_FreeType`/`FT_Done_FreeType` overhead. Store in thread-local storage.
### 6.7 Integration Point: `UISceneNode::getFontFromNamesList`
Modify the cascade so that after `FontManager::getByName()` fails for all names in the list, `SystemFontResolver::resolveFromNamesList()` is called:
```cpp
Font* UISceneNode::getFontFromNamesList( std::string_view names ) const {
Font* font = nullptr;
String::readBySeparatorStoppable(
names,
[&font]( std::string_view name ) {
name = String::trim( name, ' ' );
name = String::trim( name, '\'' );
font = FontManager::instance()->getByName( std::string{ name } );
return font == nullptr;
},
',' );
// NEW: System font fallback
if ( !font && SystemFontResolver::existsSingleton() ) {
FontDesc desc = SystemFontResolver::instance()->resolveFromNamesList(
std::string{ names }, FontWeight::Normal, false );
if ( !desc.path.empty() ) {
// Load via FontFamily or FontTrueType::New
FontTrueType* ttf = FontTrueType::New( desc.path, desc.path, desc.faceIndex );
if ( ttf && ttf->loaded() )
font = ttf;
}
}
return font;
}
```
---
## 7. Changes to CSS Shorthand Parser
### 7.1 `stylesheetspecification.cpp` — System Font Keywords
**Current state** (line 1221-1226): Six CSS system font keywords (`caption`, `icon`, `menu`, `message-box`, `small-caption`, `status-bar`) are detected but return empty.
**Fix:** When a system font keyword is detected, resolve it via `SystemFontResolver` and return the appropriate sub-properties:
```cpp
static const UnorderedMap<std::string, GenericFamily> systemFontToGeneric = {
{ "caption", GenericFamily::SystemUi },
{ "icon", GenericFamily::SystemUi },
{ "menu", GenericFamily::SystemUi },
{ "message-box", GenericFamily::SystemUi },
{ "small-caption",GenericFamily::SystemUi },
{ "status-bar", GenericFamily::SystemUi },
};
for ( const auto& sysFont : systemFontToGeneric ) {
if ( lowerVal == sysFont.first ) {
if ( SystemFontResolver::existsSingleton() ) {
FontDesc desc = SystemFontResolver::instance()->resolveGeneric(
sysFont.second, FontWeight::Normal, false );
// Build StyleSheetProperty vector with resolved family and metadata
return {
StyleSheetProperty( "font-family", desc.path ),
// ... font-style, font-size from system font metadata
};
}
return {}; // No resolver → no system fonts available
}
}
```
---
## 8. Generic Font Family Resolution
CSS defines five generic font families: `serif`, `sans-serif`, `monospace`, `cursive`, `fantasy`. Plus `system-ui`, `emoji`, `math`, `fangsong` (CSS Fonts Level 4).
### 8.1 Default Mappings Per Platform
| Generic | Windows | macOS | Linux (Fontconfig) | Android | Haiku |
|---------|---------|-------|---------------------|---------|-------|
| `serif` | "Times New Roman" | "Times" | `FC_SERIF` "DejaVu Serif" or "Liberation Serif" | "Noto Serif" | "Noto Serif" |
| `sans-serif` | "Arial" | "Helvetica" | `FC_SANS` "DejaVu Sans" or "Liberation Sans" | "Roboto" | "Noto Sans" |
| `monospace` | "Consolas" | "Menlo" | `FC_MONO` "DejaVu Sans Mono" or "Liberation Mono" | "Droid Sans Mono" | "Noto Sans Mono" |
| `cursive` | "Comic Sans MS" | "Apple Chancery" | `FC_SANS` (fallback) | "Dancing Script" | "Noto Sans" |
| `fantasy` | "Impact" | "Papyrus" | `FC_SANS` (fallback) | "Noto Sans" | "Noto Sans" |
| `system-ui` | "Segoe UI" | "SF Pro" | Fontconfig default | "Roboto" | "Noto Sans" |
| `emoji` | "Segoe UI Emoji" | "Apple Color Emoji" | "Noto Color Emoji" | "Noto Color Emoji" | "Noto Color Emoji" |
These are resolved in `resolveGeneric()` via `mGenericFallbacks`, populated per-platform in `populateGenericFallbacks()`.
### 8.2 Integration into `resolve()`
When `resolve()` is called with a family name, the matching algorithm:
1. Check `mResolveCache` (hash hit)
2. Search `mFontList` for exact family match
3. If family is a known CSS generic keyword call `resolveGeneric()`
4. If no match found try nearest family in `mFontList` (Levenshtein/prefix match)
5. Return empty `FontDesc` if nothing found
---
## 9. Thread Safety
| Structure | Access Pattern | Thread Safety |
|-----------|---------------|---------------|
| `mFontList` | Write-once on populate, read-only after | Mutex-guarded populate, lockless reads after flag set |
| `mResolveCache` | Lazy insert, concurrent reads | Fine-grained mutex per insert; reads are lockless (insertions are atomic from reader POV since we store plain structs) |
| `mCodepointFallbackCache` | Lazy insert, concurrent reads | Same as above |
| `mGenericCache` | Populated at startup, read-only after | No synchronization needed after init |
| `fontContainsCodepoint()` | Creates/destroys FT_Library per call | Must not be called concurrently for same library. Per-call FT_Library init/done is safe. |
**FreeType Library Sharing:** For `fontContainsCodepoint()`, each call creates a fresh `FT_Library`, opens the face, checks, and cleans up. This is threadsafe (FreeType 2.10+ is threadsafe when using separate libraries per thread). For optimization, a per-thread `FT_Library` thread-local can be used.
---
## 10. Implementation Order
### Phase 1: Core Interface + Stub
1. Create `include/eepp/graphics/systemfontresolver.hpp` full public interface
2. Create `src/eepp/graphics/systemfontresolver.cpp` common implementation (enumeration caching, matching/scoring, generic fallback lookup, `resolveFromNamesList`, `fontContainsCodepoint`)
3. Create no-op stubs for all five platform files (return empty lists)
4. Integrate into `premake4.lua` / build system
5. Compile and verify
### Phase 2: Platform Implementations
6. Linux/BSD Fontconfig (`systemfontresolver_linux.cpp`) test
7. Windows DirectWrite (`systemfontresolver_win.cpp`) test on Windows
8. macOS/iOS Core Text (`systemfontresolver_macos.cpp`) test on macOS
9. Android NDK API 29+ + XML fallback (`systemfontresolver_android.cpp`)
10. Haiku BFont (`systemfontresolver_haiku.cpp`)
### Phase 3: FreeType faceIndex Integration
11. Add `faceIndex` parameter to all `loadFrom*` methods (default 0, backward compatible)
12. Add `mFaceIndex` member to `FontTrueType`
13. Add `FontTrueType::New(family, path, faceIndex)` factory
### Phase 4: Fallback Glyph Routing
14. Add `mEnableSystemFallback` flag to `FontTrueType`
15. Implement `FontManager::getOrLoadSystemFallbackFont()`
16. Add OS fallback step to `FontTrueType::getGlyph()` and `getGlyphDrawable()`
17. Add codepoint fallback cache + `fontContainsCodepoint()`
### Phase 5: CSS Integration
18. Fix system font keywords in `stylesheetspecification.cpp` font shorthand parser
19. Add generic font family resolution to `getFontFromNamesList` in `uiscenenode.cpp`
20. Wire `SystemFontResolver::resolveFromNamesList()` into `getFontFromNamesList`
### Phase 6: Testing & Validation
21. Write unit tests for `SystemFontResolver` (mockable with known font fixtures)
22. Run existing font rendering tests to verify no regressions
23. Test on all platforms
---
## 11. Risk Assessment
| Risk | Severity | Mitigation |
|------|----------|------------|
| FreeType init/done overhead in `fontContainsCodepoint()` | MEDIUM | Per-thread `FT_Library` cache in thread-local; amortized across codepoint batches |
| Fontconfig being slow on first query | MEDIUM | Lazy populate; `FcFontList()` is the unavoidable first-hit cost (~60ms) |
| Android XML parsing for legacy fallback | LOW | Only on API < 29; happens once, cached |
| TTC face index mismatches | LOW | Each platform API provides the face index natively (Fontconfig: `FC_INDEX`, DirectWrite: `IDWriteFontFace::GetIndex`, Core Text: implicit) |
| Breaking existing loadFromFile callers | NONE | All `faceIndex` parameters default to `0` |
| Memory from cached system fallback fonts | LOW | `MAX_SYSTEM_FALLBACK_FONTS = 8`, capped |
| Emscripten platform | LOW | No system fonts available; resolver returns empty; CSS relies on `@font-face` or bundled fonts only |
---
## 12. Verification
### Unit Tests
- `SystemFontResolver.enumerate` verify `mFontList` is non-empty on all desktop platforms
- `SystemFontResolver.resolve_ExactFamily` query "Arial" verify path exists
- `SystemFontResolver.resolve_GenericFamily` query "sans-serif" verify valid path
- `SystemFontResolver.resolveFromNamesList` "NonExistent, sans-serif" returns sans-serif path
- `SystemFontResolver.fontContainsCodepoint` ASCII codepoints in known fonts
- `SystemFontResolver.getFallbackForCodepoint` CJK codepoint (0x65E5) valid CJK font path
### Integration Tests
- `FontTrueType.loadFromTTC` load from a .ttc file with faceIndex > 0
- `UISceneNode.getFontFromNamesList_SystemFallback` — CSS font-family with only system font names
- `FontManager.systemFallbackFonts` — verify LRU eviction behavior
### Existing Test Regression
- All font rendering tests must pass unchanged (`FontRendering.*`, `Text.*`)
- All UI layout tests must pass (system fonts only activate on codepoint miss, which existing tests should not trigger)
---
## 13. Non-Scope (Future Extensions)
- **Font variations (variable fonts with weight/width/slant axes):** The `FontQuery`/`FontDesc` structs can be extended later with axis coordinates.
- **Background font enumeration (async):** For very large font collections (1000+ fonts), `populateFontList()` could be moved to a background thread.
- **Font installation monitoring:** Detecting OS font install/uninstall at runtime and invalidating caches.
- **Emoji font discovery:** Currently hard-coded; could leverage `SystemFontResolver` to auto-detect emoji fonts.
- **PDF/print font embedding:** Using resolved file paths to embed fonts in generated PDFs.

View File

@@ -172,7 +172,7 @@
"eepp-linux": {
"build": [
{
"args": "--disable-static-build --with-mold-linker --with-debug-symbols --address-sanitizer --with-backend=SDL3 gmake",
"args": "--disable-static-build --with-mold-linker --with-debug-symbols --address-sanitizer gmake",
"command": "premake4",
"working_dir": "${project_root}"
},

View File

@@ -56,6 +56,7 @@
#include <eepp/graphics/sprite.hpp>
#include <eepp/graphics/statefuldrawable.hpp>
#include <eepp/graphics/statelistdrawable.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/graphics/textlayout.hpp>
#include <eepp/graphics/textselectionrange.hpp>

View File

@@ -10,6 +10,9 @@ using namespace EE::System;
namespace EE { namespace Graphics {
class FontTrueType;
struct FontDesc;
/** @brief The Font Manager is a singleton class that manages all the instance of fonts
instantiated. And releases the font instances automatically. So the user doesn't need to release
any font instance.
@@ -49,10 +52,13 @@ class EE_API FontManager : public ResourceManager<Font> {
Font* getByInternalId( Uint32 internalId ) const;
FontTrueType* getOrLoadSystemFallbackFont( const FontDesc& desc );
protected:
Font* mColorEmojiFont{ nullptr };
Font* mEmojiFont{ nullptr };
std::vector<Font*> mFallbackFonts;
std::vector<Font*> mSystemFallbackFonts;
FontHinting mHinting{ FontHinting::Full };
FontAntialiasing mAntialiasing{ FontAntialiasing::Grayscale };

View File

@@ -19,15 +19,19 @@ class EE_API FontTrueType : public Font {
static FontTrueType* New( const std::string& FontName, const std::string& filename );
static FontTrueType* New( const std::string& FontName, const std::string& filename,
Uint32 faceIndex );
~FontTrueType();
bool loadFromFile( const std::string& filename );
bool loadFromFile( const std::string& filename, Uint32 faceIndex = 0 );
bool loadFromMemory( const void* data, std::size_t sizeInBytes, bool copyData = true );
bool loadFromMemory( const void* data, std::size_t sizeInBytes, bool copyData = true,
Uint32 faceIndex = 0 );
bool loadFromStream( IOStream& stream );
bool loadFromStream( IOStream& stream, Uint32 faceIndex = 0 );
bool loadFromPack( Pack* pack, std::string filePackPath );
bool loadFromPack( Pack* pack, std::string filePackPath, Uint32 faceIndex = 0 );
const Font::Info& getInfo() const;
@@ -111,6 +115,10 @@ class EE_API FontTrueType : public Font {
void setEnableFallbackFont( bool enableFallbackFont );
bool isSystemFallbackEnabled() const;
void setEnableSystemFallback( bool enableSystemFallback );
bool getEnableDynamicMonospace() const;
void setEnableDynamicMonospace( bool enableDynamicMonospace );
@@ -239,6 +247,7 @@ class EE_API FontTrueType : public Font {
mutable bool mUsingFallback{ false };
bool mEnableEmojiFallback{ true };
bool mEnableFallbackFont{ true };
bool mEnableSystemFallback{ true };
bool mEnableDynamicMonospace{ false };
bool mIsBold{ false };
bool mIsItalic{ false };
@@ -246,10 +255,11 @@ class EE_API FontTrueType : public Font {
mutable UnorderedMap<unsigned int, unsigned int> mClosestCharacterSize;
mutable UnorderedMap<Uint32, Uint32> mCodePointIndexCache;
mutable UnorderedMap<Uint32, std::tuple<Uint32, Uint32, bool>> mKeyCache;
mutable UnorderedMap<Uint64, Float> mKerningCache; // For codepoints (getKerning)
mutable UnorderedMap<Uint64, Float> mKerningGlyphCache; // For glyph indices
mutable UnorderedMap<Uint64, Float> mKerningCache; // For codepoints (getKerning)
mutable UnorderedMap<Uint64, Float> mKerningGlyphCache; // For glyph indices
FontHinting mHinting{ FontHinting::Full };
FontAntialiasing mAntialiasing{ FontAntialiasing::Grayscale };
Uint32 mFaceIndex{ 0 };
FontTrueType* mFontBold{ nullptr };
FontTrueType* mFontItalic{ nullptr };
FontTrueType* mFontBoldItalic{ nullptr };

View File

@@ -0,0 +1,132 @@
#ifndef EE_GRAPHICS_SYSTEMFONTRESOLVER_HPP
#define EE_GRAPHICS_SYSTEMFONTRESOLVER_HPP
#include <eepp/config.hpp>
#include <eepp/graphics/base.hpp>
#include <eepp/system/singleton.hpp>
#include <string>
#include <vector>
namespace EE { namespace Graphics {
enum class FontWeight : Uint16 {
Thin = 100,
ExtraLight = 200,
Light = 300,
Normal = 400,
Medium = 500,
SemiBold = 600,
Bold = 700,
ExtraBold = 800,
Black = 900
};
enum class FontStretch : Uint8 {
UltraCondensed = 1,
ExtraCondensed = 2,
Condensed = 3,
SemiCondensed = 4,
Normal = 5,
SemiExpanded = 6,
Expanded = 7,
ExtraExpanded = 8,
UltraExpanded = 9
};
enum class GenericFamily : Uint8 {
None,
Serif,
SansSerif,
Monospace,
Cursive,
Fantasy,
SystemUi,
Emoji
};
struct FontQuery {
std::string family;
FontWeight weight{ FontWeight::Normal };
FontStretch stretch{ FontStretch::Normal };
bool italic{ false };
};
struct FontDesc {
std::string family;
std::string path;
Uint32 faceIndex{ 0 };
FontWeight weight{ FontWeight::Normal };
FontStretch stretch{ FontStretch::Normal };
bool italic{ false };
bool monospace{ false };
};
class EE_API SystemFontResolver {
SINGLETON_DECLARE_HEADERS( SystemFontResolver )
public:
~SystemFontResolver();
const std::vector<FontDesc>& enumerate();
std::vector<FontDesc> enumerateFamily( const std::string& family );
FontDesc resolve( const FontQuery& query );
FontDesc resolveFromNamesList( const std::string& namesList, FontWeight weight, bool italic );
FontDesc resolveGeneric( GenericFamily generic, FontWeight weight, bool italic );
FontDesc getSystemFont() const;
FontDesc getSystemMonospaceFont() const;
FontDesc getFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic );
bool fontContainsCodepoint( const std::string& path, Uint32 codepoint );
void invalidateCache();
void ensureFontListPopulated() const;
static void setEnabled( bool enabled );
static bool isEnabled();
static GenericFamily genericFamilyFromName( const std::string& name );
protected:
SystemFontResolver();
private:
void populateFontList() const;
void populateGenericFallbacks() const;
static int scoreMatch( const FontQuery& query, const FontDesc& candidate );
mutable std::vector<FontDesc> mFontList;
mutable bool mFontListPopulated{ false };
static Uint64 makeCacheKey( const std::string& normFamily, FontWeight weight,
FontStretch stretch, bool italic );
mutable UnorderedMap<Uint64, FontDesc> mResolveCache;
mutable UnorderedMap<Uint32, FontDesc> mGenericCache;
mutable UnorderedMap<Uint32, std::string> mCodepointFallbackCache;
struct GenericEntry {
GenericFamily generic;
FontDesc desc;
};
mutable std::vector<GenericEntry> mGenericFallbacks;
static bool sEnabled;
};
}} // namespace EE::Graphics
#endif

View File

@@ -736,6 +736,10 @@ class EE_API UISceneNode : public SceneNode {
Font* getFontFromNamesList( std::string_view names, Uint32 fontStyle = 0 ) const;
Font* reevaluateFontStyle( Font* currentFont, Uint32 fontStyle ) const;
void loadFontStyleVariants( Font* font, const std::string& family ) const;
protected:
friend class EE::UI::UIWindow;
friend class EE::UI::UIWidget;

View File

@@ -671,19 +671,19 @@ function generate_os_links()
table.insert( os_links, "dl" )
end
elseif os.is_real("windows") then
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid" } )
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid", "dwrite" } )
elseif os.is_real("mingw32") then
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid" } )
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid", "dwrite" } )
elseif os.is_real("mingw64") then
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid" } )
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid", "dwrite" } )
elseif os.is_real("macosx") then
multiple_insert( os_links, { "OpenGL.framework", "CoreFoundation.framework" } )
multiple_insert( os_links, { "OpenGL.framework", "CoreFoundation.framework", "CoreText.framework" } )
elseif os.is_real("freebsd") then
multiple_insert( os_links, { "rt", "pthread", "GL" } )
elseif os.is_real("haiku") then
multiple_insert( os_links, { "GL", "network" } )
elseif os.is_real("ios") then
multiple_insert( os_links, { "OpenGLES.framework", "AudioToolbox.framework", "CoreAudio.framework", "Foundation.framework", "CoreFoundation.framework", "UIKit.framework", "QuartzCore.framework", "CoreGraphics.framework", "CoreMotion.framework", "AVFoundation.framework", "GameController.framework" } )
multiple_insert( os_links, { "OpenGLES.framework", "AudioToolbox.framework", "CoreAudio.framework", "Foundation.framework", "CoreFoundation.framework", "CoreText.framework", "UIKit.framework", "QuartzCore.framework", "CoreGraphics.framework", "CoreMotion.framework", "AVFoundation.framework", "GameController.framework" } )
end
if _OPTIONS["without-mojoal"] then
@@ -1051,6 +1051,10 @@ function build_eepp( build_name )
links { "bcrypt" }
end
if os.is_real("linux") or os.is_real("bsd") then
links { "fontconfig" }
end
files { "src/eepp/core/*.cpp",
"src/eepp/math/*.cpp",
"src/eepp/system/*.cpp",

View File

@@ -512,17 +512,17 @@ function generate_os_links()
table.insert( os_links, "dl" )
end
elseif os.istarget("windows") then
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid" } )
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid", "dwrite" } )
elseif os.istarget("mingw32") then
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid" } )
multiple_insert( os_links, { "opengl32", "glu32", "gdi32", "ws2_32", "winmm", "ole32", "uuid", "dwrite" } )
elseif os.istarget("macosx") then
multiple_insert( os_links, { "OpenGL.framework", "CoreFoundation.framework" } )
multiple_insert( os_links, { "OpenGL.framework", "CoreFoundation.framework", "CoreText.framework" } )
elseif os.istarget("bsd") then
multiple_insert( os_links, { "rt", "pthread", "GL" } )
elseif os.istarget("haiku") then
multiple_insert( os_links, { "GL", "network" } )
elseif os.istarget("ios") then
multiple_insert( os_links, { "OpenGLES.framework", "AudioToolbox.framework", "CoreAudio.framework", "Foundation.framework", "CoreFoundation.framework", "UIKit.framework", "QuartzCore.framework", "CoreGraphics.framework", "CoreMotion.framework", "AVFoundation.framework", "GameController.framework" } )
multiple_insert( os_links, { "OpenGLES.framework", "AudioToolbox.framework", "CoreAudio.framework", "Foundation.framework", "CoreFoundation.framework", "CoreText.framework", "UIKit.framework", "QuartzCore.framework", "CoreGraphics.framework", "CoreMotion.framework", "AVFoundation.framework", "GameController.framework" } )
elseif os.istarget("android") then
multiple_insert( os_links, { "GLESv1_CM", "GLESv2", "log" } )
end
@@ -921,6 +921,9 @@ function build_eepp( build_name )
filter { "action:export-compile-commands", "system:macosx" }
buildoptions { "-std=c++20" }
filter "system:linux or system:bsd"
links { "fontconfig" }
filter {}
end

View File

@@ -1,5 +1,7 @@
#include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/system/filesystem.hpp>
namespace EE { namespace Graphics {
@@ -11,6 +13,7 @@ FontManager::~FontManager() {
mEmojiFont = nullptr;
mColorEmojiFont = nullptr;
mFallbackFonts.clear();
mSystemFallbackFonts.clear();
}
Graphics::Font* FontManager::add( Graphics::Font* font ) {
@@ -101,4 +104,35 @@ Font* FontManager::getByInternalId( Uint32 internalId ) const {
return nullptr;
}
FontTrueType* FontManager::getOrLoadSystemFallbackFont( const FontDesc& desc ) {
static constexpr Uint32 MAX_SYSTEM_FALLBACK_FONTS = 32;
if ( desc.path.empty() )
return nullptr;
for ( auto* font : mSystemFallbackFonts ) {
if ( font->getType() == FontType::TTF ) {
auto* ttf = static_cast<FontTrueType*>( font );
if ( ttf->getInfo().fontpath + ttf->getInfo().filename == desc.path )
return ttf;
}
}
FontTrueType* ttf = FontTrueType::New( desc.family, desc.path, desc.faceIndex );
if ( !ttf || !ttf->loaded() ) {
eeSAFE_DELETE( ttf );
return nullptr;
}
mSystemFallbackFonts.push_back( ttf );
if ( mSystemFallbackFonts.size() > MAX_SYSTEM_FALLBACK_FONTS ) {
Font* oldest = mSystemFallbackFonts.front();
mSystemFallbackFonts.erase( mSystemFallbackFonts.begin() );
eeSAFE_DELETE( oldest );
}
return ttf;
}
}} // namespace EE::Graphics

View File

@@ -1,6 +1,7 @@
#include <eepp/core/retainsymbol.hpp>
#include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/system/filesystem.hpp>
@@ -261,6 +262,13 @@ FontTrueType* FontTrueType::New( const std::string& FontName, const std::string&
return fontTrueType;
}
FontTrueType* FontTrueType::New( const std::string& FontName, const std::string& filename,
Uint32 faceIndex ) {
FontTrueType* fontTrueType = New( FontName );
fontTrueType->loadFromFile( filename, faceIndex );
return fontTrueType;
}
FontTrueType::FontTrueType( const std::string& FontName ) :
Font( FontType::TTF, FontName ),
mLibrary( NULL ),
@@ -280,12 +288,14 @@ FontTrueType::FontTrueType( const std::string& FontName ) :
mUsingFallback( false ),
mEnableEmojiFallback( true ),
mEnableFallbackFont( true ),
mEnableSystemFallback( true ),
mEnableDynamicMonospace( false ),
mIsBold( false ),
mIsItalic( false ),
mIsMonospaceCompletePending( false ),
mHinting( FontManager::instance()->getHinting() ),
mAntialiasing( FontManager::instance()->getAntialiasing() ),
mFaceIndex( 0 ),
mFontBold( nullptr ),
mFontItalic( nullptr ),
mFontBoldItalic( nullptr ),
@@ -318,7 +328,7 @@ static bool checkHasColrTable( const FT_Face& face ) {
return length > 0;
}
bool FontTrueType::loadFromFile( const std::string& filename ) {
bool FontTrueType::loadFromFile( const std::string& filename, Uint32 faceIndex ) {
if ( !FileSystem::fileExists( filename ) &&
PackManager::instance()->isFallbackToPacksActive() ) {
std::string path( filename );
@@ -327,7 +337,7 @@ bool FontTrueType::loadFromFile( const std::string& filename ) {
if ( NULL != pack ) {
Log::info( "Loading font from pack: %s", path.c_str() );
return loadFromPack( pack, path );
return loadFromPack( pack, path, faceIndex );
}
return false;
@@ -336,6 +346,8 @@ bool FontTrueType::loadFromFile( const std::string& filename ) {
// Cleanup the previous resources
cleanup();
mFaceIndex = faceIndex;
// Initialize FreeType
FT_Library library;
if ( FT_Init_FreeType( &library ) != 0 ) {
@@ -347,7 +359,8 @@ bool FontTrueType::loadFromFile( const std::string& filename ) {
// Load the new font face from the specified file
FT_Face face;
if ( FT_New_Face( static_cast<FT_Library>( mLibrary ), filename.c_str(), 0, &face ) != 0 ) {
if ( FT_New_Face( static_cast<FT_Library>( mLibrary ), filename.c_str(),
static_cast<FT_Long>( mFaceIndex ), &face ) != 0 ) {
Log::error( "Failed to load font \"%s\" (%s) (failed to create the font face)",
filename.c_str(), mFontName.c_str() );
return false;
@@ -359,7 +372,8 @@ bool FontTrueType::loadFromFile( const std::string& filename ) {
return setFontFace( face );
}
bool FontTrueType::loadFromMemory( const void* data, std::size_t sizeInBytes, bool copyData ) {
bool FontTrueType::loadFromMemory( const void* data, std::size_t sizeInBytes, bool copyData,
Uint32 faceIndex ) {
const void* ptr = data;
if ( copyData ) {
@@ -371,6 +385,8 @@ bool FontTrueType::loadFromMemory( const void* data, std::size_t sizeInBytes, bo
// Cleanup the previous resources
cleanup();
mFaceIndex = faceIndex;
// Initialize FreeType
FT_Library library;
if ( FT_Init_FreeType( &library ) != 0 ) {
@@ -383,7 +399,8 @@ bool FontTrueType::loadFromMemory( const void* data, std::size_t sizeInBytes, bo
FT_Face face;
if ( FT_New_Memory_Face( static_cast<FT_Library>( mLibrary ),
reinterpret_cast<const FT_Byte*>( ptr ),
static_cast<FT_Long>( sizeInBytes ), 0, &face ) != 0 ) {
static_cast<FT_Long>( sizeInBytes ),
static_cast<FT_Long>( mFaceIndex ), &face ) != 0 ) {
Log::error( "Failed to load font from memory (failed to create the font face)" );
return false;
}
@@ -391,10 +408,12 @@ bool FontTrueType::loadFromMemory( const void* data, std::size_t sizeInBytes, bo
return setFontFace( face );
}
bool FontTrueType::loadFromStream( IOStream& stream ) {
bool FontTrueType::loadFromStream( IOStream& stream, Uint32 faceIndex ) {
// Cleanup the previous resources
cleanup();
mFaceIndex = faceIndex;
// Initialize FreeType
FT_Library library;
if ( FT_Init_FreeType( &library ) != 0 ) {
@@ -424,7 +443,8 @@ bool FontTrueType::loadFromStream( IOStream& stream ) {
// Load the new font face from the specified stream
FT_Face face;
if ( FT_Open_Face( static_cast<FT_Library>( mLibrary ), &args, 0, &face ) != 0 ) {
if ( FT_Open_Face( static_cast<FT_Library>( mLibrary ), &args,
static_cast<FT_Long>( mFaceIndex ), &face ) != 0 ) {
Log::error( "Failed to load font from stream (failed to create the font face)" );
delete rec;
return false;
@@ -437,7 +457,7 @@ bool FontTrueType::loadFromStream( IOStream& stream ) {
return res;
}
bool FontTrueType::loadFromPack( Pack* pack, std::string filePackPath ) {
bool FontTrueType::loadFromPack( Pack* pack, std::string filePackPath, Uint32 faceIndex ) {
if ( NULL == pack )
return false;
@@ -446,7 +466,7 @@ bool FontTrueType::loadFromPack( Pack* pack, std::string filePackPath ) {
mMemCopy.clear();
if ( pack->isOpen() && pack->extractFileToMemory( filePackPath, mMemCopy ) )
ret = loadFromMemory( mMemCopy.get(), mMemCopy.length(), false );
ret = loadFromMemory( mMemCopy.get(), mMemCopy.length(), false, faceIndex );
mInfo.fontpath = FileSystem::fileRemoveFileName( filePackPath );
mInfo.filename = FileSystem::fileNameFromPath( filePackPath );
@@ -615,6 +635,23 @@ Glyph FontTrueType::getGlyph( Uint32 codePoint, unsigned int characterSize, bool
}
}
if ( 0 == idx && mEnableSystemFallback && SystemFontResolver::existsSingleton() ) {
FontDesc fallbackDesc = SystemFontResolver::instance()->getFallbackForCodepoint(
codePoint, FontWeight::Normal, false );
if ( !fallbackDesc.path.empty() ) {
FontTrueType* systemFallback =
FontManager::instance()->getOrLoadSystemFallbackFont( fallbackDesc );
if ( systemFallback && ( idx = systemFallback->getGlyphIndex( codePoint ) ) ) {
if ( mIsMonospace && mEnableDynamicMonospace ) {
mIsMonospaceComplete = false;
mUsingFallback = true;
}
return systemFallback->getGlyphByIndex(
idx, characterSize, bold, italic, outlineThickness, getPage( characterSize ) );
}
}
}
return getGlyphByIndex( idx, characterSize, bold, italic, outlineThickness );
}
@@ -746,6 +783,26 @@ GlyphDrawable* FontTrueType::getGlyphDrawable( Uint32 codePoint, unsigned int ch
glyphIndex = getGlyphIndex( codePoint );
}
if ( 0 == glyphIndex && mEnableSystemFallback && SystemFontResolver::existsSingleton() ) {
FontDesc fallbackDesc = SystemFontResolver::instance()->getFallbackForCodepoint(
codePoint, FontWeight::Normal, false );
if ( !fallbackDesc.path.empty() ) {
FontTrueType* systemFallback =
FontManager::instance()->getOrLoadSystemFallbackFont( fallbackDesc );
if ( systemFallback &&
( tGlyphIndex = systemFallback->getGlyphIndex( codePoint ) ) ) {
glyphIndex = tGlyphIndex;
fontInternalId = systemFallback->getFontInternalId();
if ( mIsMonospace && mEnableDynamicMonospace ) {
mIsMonospaceComplete = false;
mUsingFallback = true;
}
}
}
if ( 0 == glyphIndex )
glyphIndex = getGlyphIndex( codePoint );
}
mKeyCache[codePointKey] = { fontInternalId, glyphIndex, isItalic };
}
@@ -1059,6 +1116,7 @@ void FontTrueType::cleanup() {
mStroker = NULL;
mHBFont = NULL;
mStreamRec = NULL;
mFaceIndex = 0;
mInfo = Info();
mFontInternalId = 0;
mBoldAdvanceSameAsRegular = false;
@@ -1072,6 +1130,7 @@ void FontTrueType::cleanup() {
mUsingFallback = false;
mEnableEmojiFallback = true;
mEnableFallbackFont = true;
mEnableSystemFallback = true;
mEnableDynamicMonospace = false;
mIsBold = false;
mIsItalic = false;
@@ -1624,6 +1683,14 @@ void FontTrueType::setEnableFallbackFont( bool enableFallbackFont ) {
mEnableFallbackFont = enableFallbackFont;
}
bool FontTrueType::isSystemFallbackEnabled() const {
return mEnableSystemFallback;
}
void FontTrueType::setEnableSystemFallback( bool enableSystemFallback ) {
mEnableSystemFallback = enableSystemFallback;
}
bool FontTrueType::isEmojiFallbackEnabled() const {
return mEnableEmojiFallback;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/system/log.hpp>
#include <eepp/ui/css/propertyspecification.hpp>
#include <eepp/ui/css/stylesheetspecification.hpp>
@@ -1348,8 +1349,26 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() {
static const std::string systemFonts[] = { "caption", "icon", "menu",
"message-box", "small-caption", "status-bar" };
for ( const auto& sysFont : systemFonts ) {
if ( lowerVal == sysFont )
return {};
if ( lowerVal == sysFont ) {
std::vector<StyleSheetProperty> properties;
const std::vector<std::string>& propNames = shorthand->getProperties();
int familyPos = getIndexEndingWith( propNames, "-family" );
int stylePos = getIndexEndingWith( propNames, "-style" );
if ( familyPos != -1 && Graphics::SystemFontResolver::isEnabled() ) {
Graphics::FontDesc desc =
Graphics::SystemFontResolver::instance()->resolveGeneric(
Graphics::GenericFamily::SystemUi, Graphics::FontWeight::Normal,
false );
if ( !desc.family.empty() ) {
properties.emplace_back(
StyleSheetProperty( propNames[familyPos], desc.family ) );
if ( stylePos != -1 )
properties.emplace_back( StyleSheetProperty(
propNames[stylePos], desc.italic ? "italic" : "normal" ) );
}
}
return properties;
}
}
std::vector<StyleSheetProperty> properties;

View File

@@ -2923,6 +2923,10 @@ UICodeEditor* UICodeEditor::setFontStyle( const Uint32& fontStyle ) {
mFontStyleConfig.Style = fontStyle;
invalidateDraw();
onFontStyleChanged();
if ( auto* newFont =
getUISceneNode()->reevaluateFontStyle( mFontStyleConfig.Font, fontStyle ) )
setFont( newFont );
}
return this;
@@ -3013,7 +3017,8 @@ bool UICodeEditor::applyProperty( const StyleSheetProperty& attribute ) {
setFontSelectionBackColor( attribute.asColor() );
break;
case PropertyId::FontFamily: {
Font* font = getUISceneNode()->getFontFromNamesList( attribute.value() );
Font* font =
getUISceneNode()->getFontFromNamesList( attribute.value(), getFontStyle() );
if ( NULL != font && font->loaded() ) {
setFont( font );
}

View File

@@ -186,7 +186,7 @@ bool UIConsole::applyProperty( const StyleSheetProperty& attribute ) {
setFontSelectionBackColor( attribute.asColor() );
break;
case PropertyId::FontFamily: {
Font* font = getUISceneNode()->getFontFromNamesList( attribute.value() );
Font* font = getUISceneNode()->getFontFromNamesList( attribute.value(), getFontStyleConfig().getFontStyle() );
if ( NULL != font && font->loaded() ) {
setFont( font );

View File

@@ -518,6 +518,10 @@ UIRichText* UIRichText::setFontStyle( const Uint32& fontStyle ) {
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
updateDefaultSpansStyle();
if ( auto* newFont = getUISceneNode()->reevaluateFontStyle(
mRichText.getFontStyleConfig().Font, fontStyle ) )
setFont( newFont );
}
return this;
}

View File

@@ -2,6 +2,7 @@
#include <eepp/core/string.hpp>
#include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/network/http.hpp>
#include <eepp/network/uri.hpp>
@@ -1515,6 +1516,7 @@ Font* UISceneNode::getFontFromNamesList( std::string_view names, Uint32 fontStyl
[&]( std::string_view name ) {
name = String::trim( name, ' ' );
name = String::trim( name, '\'' );
name = String::trim( name, '"' );
std::string fontFamily{ name };
if ( fontStyle )
fontFamily += "#" + Text::styleFlagToString( fontStyle );
@@ -1522,7 +1524,113 @@ Font* UISceneNode::getFontFromNamesList( std::string_view names, Uint32 fontStyl
return font != nullptr;
},
',' );
if ( !font && Graphics::SystemFontResolver::isEnabled() ) {
Graphics::FontWeight weight =
( fontStyle & Text::Bold ) ? Graphics::FontWeight::Bold : Graphics::FontWeight::Normal;
Graphics::FontDesc desc = Graphics::SystemFontResolver::instance()->resolveFromNamesList(
std::string{ names }, weight, fontStyle & Text::Italic );
if ( !desc.path.empty() ) {
std::string family = desc.family;
if ( fontStyle )
family += "#" + Text::styleFlagToString( fontStyle );
FontTrueType* ttf = FontTrueType::New( family, desc.path, desc.faceIndex );
if ( ttf && ttf->loaded() ) {
font = ttf;
Uint32 weightStyle = fontStyle & ( Text::Bold | Text::Italic );
if ( weightStyle ) {
Font* regular =
FontManager::instance()->getByName( desc.family );
if ( regular && regular != font &&
regular->getType() == FontType::TTF ) {
auto* regularFT = static_cast<FontTrueType*>( regular );
if ( weightStyle == Text::Bold )
regularFT->setBoldFont( ttf );
else if ( weightStyle == Text::Italic )
regularFT->setItalicFont( ttf );
else
regularFT->setBoldItalicFont( ttf );
}
}
}
}
}
return font;
}
Font* UISceneNode::reevaluateFontStyle( Font* currentFont, Uint32 fontStyle ) const {
if ( !currentFont || !Graphics::SystemFontResolver::isEnabled() )
return nullptr;
if ( currentFont->getType() != FontType::TTF )
return nullptr;
std::string name = currentFont->getName();
size_t pos = name.find( '#' );
if ( pos != std::string::npos )
name = name.substr( 0, pos );
Uint32 weightStyle = fontStyle & ( Text::Bold | Text::Italic );
Font* newFont = getFontFromNamesList( name, weightStyle );
if ( newFont && newFont != currentFont )
return newFont;
return nullptr;
}
void UISceneNode::loadFontStyleVariants( Font* font, const std::string& family ) const {
if ( !font || !Graphics::SystemFontResolver::isEnabled() )
return;
if ( font->getType() != FontType::TTF )
return;
auto* ft = static_cast<FontTrueType*>( font );
auto loadVariant = [this, family]( FontWeight weight, bool italic ) -> FontTrueType* {
Uint32 style = 0;
if ( italic )
style |= Text::Italic;
if ( weight == FontWeight::Bold )
style |= Text::Bold;
std::string queryFamily = family;
if ( style )
queryFamily += "#" + Text::styleFlagToString( style );
Font* existing = FontManager::instance()->getByName( queryFamily );
if ( existing && existing->getType() == FontType::TTF )
return static_cast<FontTrueType*>( existing );
FontDesc desc = SystemFontResolver::instance()->resolveGeneric(
SystemFontResolver::genericFamilyFromName( family ), weight, italic );
if ( desc.path.empty() ) {
FontQuery query;
query.family = family;
query.weight = weight;
query.italic = italic;
desc = SystemFontResolver::instance()->resolve( query );
}
if ( desc.path.empty() )
return nullptr;
auto* ttf = FontTrueType::New( queryFamily, desc.path, desc.faceIndex );
if ( !ttf || !ttf->loaded() ) {
eeSAFE_DELETE( ttf );
return nullptr;
}
return ttf;
};
FontTrueType* boldFont = loadVariant( FontWeight::Bold, false );
if ( boldFont )
ft->setBoldFont( boldFont );
FontTrueType* italicFont = loadVariant( FontWeight::Normal, true );
if ( italicFont )
ft->setItalicFont( italicFont );
FontTrueType* boldItalicFont = loadVariant( FontWeight::Bold, true );
if ( boldItalicFont )
ft->setBoldItalicFont( boldItalicFont );
}
}} // namespace EE::UI

View File

@@ -15,8 +15,7 @@
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
namespace EE {
namespace UI {
namespace EE { namespace UI {
UITextSpan* UITextSpan::New() {
return eeNew( UITextSpan, () );
@@ -128,7 +127,8 @@ bool UITextSpan::applyProperty( const StyleSheetProperty& attribute ) {
setFontShadowOffset( attribute.asVector2f() );
break;
case PropertyId::FontFamily: {
Font* font = getUISceneNode()->getFontFromNamesList( attribute.value() );
Font* font =
getUISceneNode()->getFontFromNamesList( attribute.value(), getFontStyle() );
if ( NULL != font && font->loaded() ) {
setFont( font );
}
@@ -276,6 +276,10 @@ UITextSpan* UITextSpan::setFontStyle( const Uint32& fontStyle ) {
mRichText.invalidate();
onFontStyleChanged();
notifyLayoutAttrChange();
if ( auto* newFont = getUISceneNode()->reevaluateFontStyle(
mRichText.getFontStyleConfig().getFont(), fontStyle ) )
setFont( newFont );
}
return this;
}
@@ -819,5 +823,4 @@ void UILabelSpan::activateTarget() {
}
}
}
} // namespace EE::UI
}} // namespace EE::UI

View File

@@ -179,6 +179,10 @@ UITextView* UITextView::setFontStyle( const Uint32& fontStyle ) {
onFontStyleChanged();
notifyLayoutAttrChange();
invalidateDraw();
if ( auto* newFont =
getUISceneNode()->reevaluateFontStyle( mFontStyleConfig.Font, fontStyle ) )
setFont( newFont );
}
return this;
@@ -742,7 +746,8 @@ bool UITextView::applyProperty( const StyleSheetProperty& attribute ) {
setSelectionBackColor( attribute.asColor() );
break;
case PropertyId::FontFamily: {
Font* font = getUISceneNode()->getFontFromNamesList( attribute.value() );
Font* font =
getUISceneNode()->getFontFromNamesList( attribute.value(), getFontStyle() );
if ( !mUsingCustomStyling && NULL != font && font->loaded() ) {
setFont( font );
}

View File

@@ -371,6 +371,9 @@ UITooltip* UITooltip::setFontStyle( const Uint32& fontStyle ) {
onAutoSize();
autoAlign();
invalidateDraw();
if ( auto* newFont = getUISceneNode()->reevaluateFontStyle( mStyleConfig.Font, fontStyle ) )
setFont( newFont );
}
return this;
@@ -560,7 +563,8 @@ bool UITooltip::applyProperty( const StyleSheetProperty& attribute ) {
setFontShadowOffset( attribute.asVector2f() );
break;
case PropertyId::FontFamily: {
Font* font = getUISceneNode()->getFontFromNamesList( attribute.value() );
Font* font =
getUISceneNode()->getFontFromNamesList( attribute.value(), getFontStyle() );
if ( !mUsingCustomStyling && NULL != font && font->loaded() )
setFont( font );

View File

@@ -4,6 +4,7 @@
#include <iostream>
EE_MAIN_FUNC int main( int argc, char** argv ) {
SystemFontResolver::setEnabled( true );
args::ArgumentParser parser( "eepp HTML Example" );
args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } );

View File

@@ -0,0 +1,536 @@
#include "utest.hpp"
#include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
using namespace EE;
using namespace EE::Graphics;
using namespace EE::System;
static std::string getFontsDir() {
return Sys::getProcessPath() + "../assets/fonts/";
}
UTEST( SystemFontResolver, singletonLifecycle ) {
SystemFontResolver::setEnabled( true );
UTEST_PRINT_STEP( "Create singleton" );
auto* resolver = SystemFontResolver::createSingleton();
EXPECT_TRUE( resolver != nullptr );
EXPECT_TRUE( SystemFontResolver::existsSingleton() != nullptr );
UTEST_PRINT_STEP( "Destroy singleton" );
SystemFontResolver::destroySingleton();
EXPECT_TRUE( SystemFontResolver::existsSingleton() == nullptr );
UTEST_PRINT_STEP( "Re-create via instance()" );
auto* resolver2 = SystemFontResolver::instance();
EXPECT_TRUE( resolver2 != nullptr );
EXPECT_TRUE( SystemFontResolver::existsSingleton() != nullptr );
SystemFontResolver::destroySingleton();
SystemFontResolver::setEnabled( false );
}
UTEST( SystemFontResolver, genericFamilyFromName ) {
SystemFontResolver::setEnabled( true );
EXPECT_EQ( GenericFamily::Serif, SystemFontResolver::genericFamilyFromName( "serif" ) );
EXPECT_EQ( GenericFamily::SansSerif,
SystemFontResolver::genericFamilyFromName( "sans-serif" ) );
EXPECT_EQ( GenericFamily::Monospace, SystemFontResolver::genericFamilyFromName( "monospace" ) );
EXPECT_EQ( GenericFamily::Cursive, SystemFontResolver::genericFamilyFromName( "cursive" ) );
EXPECT_EQ( GenericFamily::Fantasy, SystemFontResolver::genericFamilyFromName( "fantasy" ) );
EXPECT_EQ( GenericFamily::SystemUi, SystemFontResolver::genericFamilyFromName( "system-ui" ) );
EXPECT_EQ( GenericFamily::Emoji, SystemFontResolver::genericFamilyFromName( "emoji" ) );
EXPECT_EQ( GenericFamily::None,
SystemFontResolver::genericFamilyFromName( "bogus-font-name" ) );
EXPECT_EQ( GenericFamily::None, SystemFontResolver::genericFamilyFromName( "" ) );
EXPECT_EQ( GenericFamily::SansSerif,
SystemFontResolver::genericFamilyFromName( "SANS-SERIF" ) );
EXPECT_EQ( GenericFamily::Monospace,
SystemFontResolver::genericFamilyFromName( " monospace " ) );
SystemFontResolver::destroySingleton();
SystemFontResolver::setEnabled( false );
}
UTEST( SystemFontResolver, enumerate ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
const auto& fonts = resolver->enumerate();
UTEST_PRINTF( "Enumerated %zu system fonts\n", fonts.size() );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD
EXPECT_TRUE_MSG( fonts.size() > 0, "Fontconfig should find fonts on Linux/BSD" );
#elif EE_PLATFORM == EE_PLATFORM_WIN
EXPECT_TRUE_MSG( fonts.size() > 0, "DirectWrite should find fonts on Windows" );
#elif EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_IOS
EXPECT_TRUE_MSG( fonts.size() > 0, "CoreText should find fonts on macOS/iOS" );
#endif
for ( const auto& desc : fonts ) {
EXPECT_FALSE_MSG( desc.path.empty(),
( "Font path must not be empty for family: " + desc.family ).c_str() );
break;
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, enumerateFamily ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
auto fonts = resolver->enumerateFamily( "DejaVu Sans" );
UTEST_PRINTF( "Found %zu DejaVu Sans fonts\n", fonts.size() );
for ( const auto& desc : fonts ) {
EXPECT_FALSE( desc.path.empty() );
}
if ( !fonts.empty() ) {
UTEST_PRINT_STEP( "Verify faceIndex is set" );
EXPECT_EQ( 0, fonts[0].faceIndex );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, findVerdana ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
FontQuery query;
query.family = "Verdana";
query.weight = FontWeight::Normal;
query.italic = false;
FontDesc desc = resolver->resolve( query );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS
if ( resolver->enumerate().size() > 0 ) {
if ( !desc.path.empty() ) {
EXPECT_STDSTREQ( "Verdana", desc.family );
}
}
#endif
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveFromNamesList ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve with fallback to sans-serif" );
FontDesc desc = resolver->resolveFromNamesList( "NonExistentFont12345, sans-serif",
FontWeight::Normal, false );
#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
EXPECT_TRUE_MSG( desc.path.empty(), "Emscripten has no system fonts" );
#else
if ( !desc.path.empty() ) {
EXPECT_FALSE( desc.family.empty() );
}
#endif
UTEST_PRINT_STEP( "Resolve empty string" );
FontDesc descEmpty = resolver->resolveFromNamesList( "", FontWeight::Normal, false );
EXPECT_TRUE( descEmpty.path.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolve ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve a known family" );
FontQuery query;
query.family = "sans-serif";
query.weight = FontWeight::Normal;
query.italic = false;
FontDesc desc = resolver->resolve( query );
if ( !desc.path.empty() ) {
EXPECT_FALSE( desc.family.empty() );
}
UTEST_PRINT_STEP( "Resolve with bogus family" );
query.family = "zzz_not_a_real_font_family_zzz";
FontDesc descBogus = resolver->resolve( query );
EXPECT_TRUE( descBogus.path.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGeneric ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve monospace" );
FontDesc descMono =
resolver->resolveGeneric( GenericFamily::Monospace, FontWeight::Normal, false );
UTEST_PRINTF( "Monospace default: %s at %s\n", descMono.family.c_str(), descMono.path.c_str() );
UTEST_PRINT_STEP( "Resolve sans-serif" );
FontDesc descSans =
resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
UTEST_PRINT_STEP( "Resolve serif" );
FontDesc descSerif =
resolver->resolveGeneric( GenericFamily::Serif, FontWeight::Normal, false );
UTEST_PRINT_STEP( "Resolve None" );
FontDesc descNone = resolver->resolveGeneric( GenericFamily::None, FontWeight::Normal, false );
EXPECT_TRUE( descNone.path.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, fontContainsCodepoint ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE_MSG( FileSystem::fileExists( fontPath ),
( "Font file not found: " + fontPath ).c_str() );
UTEST_PRINT_STEP( "ASCII letter 'A'" );
EXPECT_TRUE( resolver->fontContainsCodepoint( fontPath, 'A' ) );
UTEST_PRINT_STEP( "CJK character U+65E5 (日)" );
bool hasCJK = resolver->fontContainsCodepoint( fontPath, 0x65E5 );
UTEST_PRINTF( "DejaVuSansMono has CJK U+65E5: %s\n", hasCJK ? "yes" : "no" );
UTEST_PRINT_STEP( "Non-existent file" );
EXPECT_FALSE( resolver->fontContainsCodepoint( "/nonexistent/font.ttf", 'A' ) );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, getFallbackForCodepoint ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Look up fallback for CJK U+65E5 (日)" );
FontDesc desc = resolver->getFallbackForCodepoint( 0x65E5, FontWeight::Normal, false );
if ( !desc.path.empty() ) {
UTEST_PRINTF( "Fallback for U+65E5: %s (%s)\n", desc.family.c_str(), desc.path.c_str() );
EXPECT_FALSE( desc.family.empty() );
}
UTEST_PRINT_STEP( "Look up fallback for Arabic U+0627 (ا)" );
FontDesc descArabic = resolver->getFallbackForCodepoint( 0x0627, FontWeight::Normal, false );
if ( !descArabic.path.empty() ) {
UTEST_PRINTF( "Fallback for U+0627: %s (%s)\n", descArabic.family.c_str(),
descArabic.path.c_str() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, invalidateCache ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Cache a resolve result" );
FontQuery query;
query.family = "sans-serif";
query.weight = FontWeight::Normal;
FontDesc desc1 = resolver->resolve( query );
UTEST_PRINT_STEP( "Invalidate cache" );
resolver->invalidateCache();
UTEST_PRINT_STEP( "Re-resolve after invalidation" );
FontDesc desc2 = resolver->resolve( query );
EXPECT_STDSTREQ( desc1.path, desc2.path );
EXPECT_STDSTREQ( desc1.family, desc2.family );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, getSystemFont ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
FontDesc sysFont = resolver->getSystemFont();
FontDesc monoFont = resolver->getSystemMonospaceFont();
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, genericFallbackPurity ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
std::vector<const char*> scriptSuffixes = {
"georgian", "cjk", "arabic", "hebrew", "armenian", "lao", "thai",
"devanagari", "tamil", "bengali", "gurmukhi", "gujarati", "oriya",
"telugu", "kannada", "malayalam", "sinhala", "khmer", "tibetan",
"myanmar", "ethiopic", "cherokee", "canadian", "mongolian", "yi",
"nko", "tifinagh", "vai", "bamum", "coptic", "glagolitic", "gothic",
"old", "ugaritic", "osmanya", "osmanya", "phags", "syloti"
};
auto checkFamily = [&scriptSuffixes]( const FontDesc& desc, const char* genericName ) {
if ( desc.path.empty() )
return;
std::string lowerFamily = String::toLower( desc.family );
for ( const char* suffix : scriptSuffixes ) {
std::string suffixStr( suffix );
if ( lowerFamily.find( suffixStr ) != std::string::npos ) {
UTEST_PRINTF( "WARNING: %s fallback resolved to script-specific font: %s\n",
genericName, desc.family.c_str() );
}
}
};
UTEST_PRINT_STEP( "Check serif generic fallback" );
FontDesc serif = resolver->resolveGeneric( GenericFamily::Serif, FontWeight::Normal, false );
checkFamily( serif, "serif" );
UTEST_PRINT_STEP( "Check sans-serif generic fallback" );
FontDesc sans = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
checkFamily( sans, "sans-serif" );
UTEST_PRINT_STEP( "Check monospace generic fallback" );
FontDesc mono = resolver->resolveGeneric( GenericFamily::Monospace, FontWeight::Normal, false );
checkFamily( mono, "monospace" );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveFromNamesListRealWorld ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve with double-quoted font names" );
FontDesc desc1 = resolver->resolveFromNamesList(
"\"Helvetica Neue\", Arial, Helvetica, \"Nimbus Sans L\", sans-serif",
FontWeight::Normal, false );
UTEST_PRINTF( "Resolved: %s (%s)\n", desc1.family.c_str(), desc1.path.c_str() );
UTEST_PRINT_STEP( "Resolve with single-quoted font names" );
FontDesc desc2 = resolver->resolveFromNamesList(
"'Times New Roman', serif",
FontWeight::Normal, false );
UTEST_PRINTF( "Resolved: %s (%s)\n", desc2.family.c_str(), desc2.path.c_str() );
UTEST_PRINT_STEP( "Resolve with mixture of quoted and unquoted" );
FontDesc desc3 = resolver->resolveFromNamesList(
"Roboto, \"Helvetica Neue\", Arial, sans-serif",
FontWeight::Normal, false );
UTEST_PRINTF( "Resolved: %s (%s)\n", desc3.family.c_str(), desc3.path.c_str() );
UTEST_PRINT_STEP( "Georgia, serif (regression: substring overmatch)" );
FontDesc desc4 = resolver->resolveFromNamesList(
"Georgia, \"Bitstream Charter\", serif",
FontWeight::Normal, false );
UTEST_PRINTF( "Resolved: %s (%s)\n", desc4.family.c_str(), desc4.path.c_str() );
if ( !desc4.path.empty() ) {
std::string lower = String::toLower( desc4.family );
EXPECT_TRUE_MSG( lower == "georgia" || lower == "times new roman" ||
lower.find( "serif" ) != std::string::npos,
( "Should resolve to Georgia or a serif font, got: " + desc4.family )
.c_str() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGenericWeights ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve sans-serif at multiple weights" );
FontDesc normal = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
FontDesc bold = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Bold, false );
FontDesc light = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Light, false );
FontDesc black = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Black, false );
UTEST_PRINTF( "Normal: %s\n", normal.family.c_str() );
UTEST_PRINTF( "Bold: %s\n", bold.family.c_str() );
UTEST_PRINTF( "Light: %s\n", light.family.c_str() );
UTEST_PRINTF( "Black: %s\n", black.family.c_str() );
if ( !normal.path.empty() )
EXPECT_FALSE( normal.family.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGenericWeightPreference ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Normal weight should prefer Normal or close weight" );
FontDesc normal = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
if ( !normal.path.empty() ) {
int diff = eeabs( (int)normal.weight - (int)FontWeight::Normal );
EXPECT_TRUE_MSG( diff <= 300,
( "Normal weight diff too large: " + normal.family + " weight=" +
String::toString( (int)normal.weight ) )
.c_str() );
}
UTEST_PRINT_STEP( "Bold weight should prefer Bold or close weight" );
FontDesc bold = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Bold, false );
if ( !bold.path.empty() ) {
int diff = eeabs( (int)bold.weight - (int)FontWeight::Bold );
UTEST_PRINTF( "Bold: %s (weight=%d diff=%d)\n", bold.family.c_str(), (int)bold.weight, diff );
}
UTEST_PRINT_STEP( "Light weight should prefer Light or close weight" );
FontDesc light = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Light, false );
if ( !light.path.empty() ) {
int diff = eeabs( (int)light.weight - (int)FontWeight::Light );
UTEST_PRINTF( "Light: %s (weight=%d diff=%d)\n", light.family.c_str(), (int)light.weight, diff );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGenericItalicFallback ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Request italic — should return a font even if no italic variant" );
FontDesc italic = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, true );
if ( !italic.path.empty() ) {
UTEST_PRINTF( "Italic: %s (italic=%d)\n", italic.family.c_str(), (int)italic.italic );
EXPECT_FALSE( italic.family.empty() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveBoldFromNamesList ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve Arial with bold weight" );
FontDesc desc = resolver->resolveFromNamesList( "Arial, sans-serif", FontWeight::Bold, false );
UTEST_PRINTF( "Bold: %s (weight=%d)\n", desc.family.c_str(), (int)desc.weight );
if ( !desc.path.empty() ) {
EXPECT_FALSE( desc.family.empty() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, invalidateCachePersistence ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "First resolve" );
FontQuery query;
query.family = "sans-serif";
query.weight = FontWeight::Normal;
FontDesc first = resolver->resolve( query );
UTEST_PRINTF( "First: %s (%s)\n", first.family.c_str(), first.path.c_str() );
UTEST_PRINT_STEP( "Invalidate cache" );
resolver->invalidateCache();
UTEST_PRINT_STEP( "Second resolve after invalidation" );
FontDesc second = resolver->resolve( query );
UTEST_PRINTF( "Second: %s (%s)\n", second.family.c_str(), second.path.c_str() );
EXPECT_STDSTREQ( first.path, second.path );
EXPECT_STDSTREQ( first.family, second.family );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, glyphFallbackRoundTrip ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Look up fallback for CJK U+65E5" );
FontDesc cjk = resolver->getFallbackForCodepoint( 0x65E5, FontWeight::Normal, false );
if ( !cjk.path.empty() ) {
UTEST_PRINTF( "CJK fallback: %s\n", cjk.family.c_str() );
EXPECT_TRUE( resolver->fontContainsCodepoint( cjk.path, 0x65E5 ) );
}
UTEST_PRINT_STEP( "Look up fallback for Arabic U+0627" );
FontDesc arabic = resolver->getFallbackForCodepoint( 0x0627, FontWeight::Normal, false );
if ( !arabic.path.empty() ) {
UTEST_PRINTF( "Arabic fallback: %s\n", arabic.family.c_str() );
EXPECT_TRUE( resolver->fontContainsCodepoint( arabic.path, 0x0627 ) );
}
UTEST_PRINT_STEP( "Verify ASCII fallback" );
FontDesc ascii = resolver->getFallbackForCodepoint( 'A', FontWeight::Normal, false );
if ( !ascii.path.empty() ) {
UTEST_PRINTF( "ASCII fallback: %s\n", ascii.family.c_str() );
EXPECT_TRUE( resolver->fontContainsCodepoint( ascii.path, 'A' ) );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( FontTrueType_faceIndex, loadWithDefaultFaceIndex ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE( FileSystem::fileExists( fontPath ) );
FontTrueType* font = FontTrueType::New( "Test-faceIndex-default" );
bool loaded = font->loadFromFile( fontPath );
ASSERT_TRUE( loaded );
EXPECT_TRUE( font->loaded() );
eeDelete( font );
}
UTEST( FontTrueType_faceIndex, newWithFaceIndex ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE( FileSystem::fileExists( fontPath ) );
FontTrueType* font = FontTrueType::New( "Test-faceIndex-explicit", fontPath, 0 );
ASSERT_TRUE( font != nullptr );
EXPECT_TRUE( font->loaded() );
eeDelete( font );
}
UTEST( FontTrueType_faceIndex, loadFromMemoryFaceIndex ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE( FileSystem::fileExists( fontPath ) );
ScopedBuffer buf;
FileSystem::fileGet( fontPath, buf );
FontTrueType* font = FontTrueType::New( "Test-faceIndex-memory" );
bool loaded = font->loadFromMemory( buf.get(), buf.length(), true, 0 );
ASSERT_TRUE( loaded );
EXPECT_TRUE( font->loaded() );
eeDelete( font );
}