mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-31 10:36:30 +03:00
Initial work on the system font resolver.
This commit is contained in:
710
.agent/plans/system_font_resolver_plan.md
Normal file
710
.agent/plans/system_font_resolver_plan.md
Normal 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.
|
||||
@@ -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}"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
132
include/eepp/graphics/systemfontresolver.hpp
Normal file
132
include/eepp/graphics/systemfontresolver.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
14
premake4.lua
14
premake4.lua
@@ -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",
|
||||
|
||||
11
premake5.lua
11
premake5.lua
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
1006
src/eepp/graphics/systemfontresolver.cpp
Normal file
1006
src/eepp/graphics/systemfontresolver.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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" } );
|
||||
|
||||
|
||||
536
src/tests/unit_tests/systemfontresolver_tests.cpp
Normal file
536
src/tests/unit_tests/systemfontresolver_tests.cpp
Normal 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 );
|
||||
}
|
||||
Reference in New Issue
Block a user