Optimize native system font resolution. Fix duplicate macOS menu shortcuts and suppress popup hint.

Avoid enumerating every installed font during SystemFontResolver warmup.
Use native platform matching for generic families and lazily resolve
fallback fonts when a missing glyph is encountered.

Cache generic and codepoint fallback results, preserve collection face
indices, and validate native matches with FreeType. Improve CoreText,
DirectWrite, and Fontconfig matching while retaining full enumeration
for font-listing APIs.

Add a Fontconfig override for testing the backend on macOS and extend
resolver coverage for lazy matching, CJK fallback, and enumeration.

Make pixel-exact subpixel rendering tests disable HiDPI so their
framebuffer dimensions remain deterministic on Retina displays.

Avoid executing native menu key equivalents twice when SDL also forwards
the same key-down event through eepp's keybinding dispatcher.

Preserve native menu item activation for mouse clicks.

Hide the "Hold Shift to keep menu open" hint while the native macOS menu
bar is enabled.
This commit is contained in:
Martín Lucas Golini
2026-09-01 11:56:49 -03:00
parent cff165fa37
commit f8c324b1f1
5 changed files with 768 additions and 297 deletions
+14 -4
View File
@@ -110,11 +110,11 @@ class EE_API SystemFontResolver {
FontDesc getFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic ); FontDesc getFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic );
/** Populate and cache the system font database without copying the resulting font list. /** Pre-resolve the native fonts needed by normal rendering. Full system-font enumeration
* Safe to call from a worker thread after enabling the resolver. */ * remains lazy until enumerate() or enumerateFamily() is called. */
void warmUp() const; void warmUp() const;
bool fontContainsCodepoint( const std::string& path, Uint32 codepoint ); bool fontContainsCodepoint( const std::string& path, Uint32 codepoint, Uint32 faceIndex = 0 );
void invalidateCache(); void invalidateCache();
@@ -122,6 +122,10 @@ class EE_API SystemFontResolver {
bool isLoading() const { return mFontListLoading; } bool isLoading() const { return mFontListLoading; }
bool isFontListPopulated() const {
return mFontListPopulated.load( std::memory_order_acquire );
}
std::vector<FontDesc> enumerate(); std::vector<FontDesc> enumerate();
std::vector<FontDesc> enumerateFamily( const std::string& family ); std::vector<FontDesc> enumerateFamily( const std::string& family );
@@ -148,6 +152,12 @@ class EE_API SystemFontResolver {
void populateGenericFallbacks() const; void populateGenericFallbacks() const;
FontDesc matchFont( const FontQuery& query ) const;
FontDesc matchGenericFont( GenericFamily generic, FontWeight weight, bool italic ) const;
FontDesc resolveGenericCached( GenericFamily generic, FontWeight weight, bool italic ) const;
FontDesc matchFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic ) const; FontDesc matchFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic ) const;
static int scoreMatch( const FontQuery& query, const FontDesc& candidate ); static int scoreMatch( const FontQuery& query, const FontDesc& candidate );
@@ -164,7 +174,7 @@ class EE_API SystemFontResolver {
mutable UnorderedMap<Uint32, FontDesc> mGenericCache; mutable UnorderedMap<Uint32, FontDesc> mGenericCache;
mutable UnorderedMap<Uint32, std::string> mCodepointFallbackCache; mutable UnorderedMap<Uint32, FontDesc> mCodepointFallbackCache;
struct GenericEntry { struct GenericEntry {
GenericFamily generic; GenericFamily generic;
File diff suppressed because it is too large Load Diff
@@ -495,6 +495,13 @@ struct NativeMenuSource {
- (void)menuItemActivated:(NSMenuItem*)sender { - (void)menuItemActivated:(NSMenuItem*)sender {
eeASSERT( [NSThread isMainThread] ); eeASSERT( [NSThread isMainThread] );
// Shortcut text on UIMenuItem is presentation-only; applications register the actual shortcut
// with their KeyBindings target. AppKit also invokes this action for native key equivalents,
// while SDL still forwards the same key-down event to eepp. Let the normal keybinding dispatch
// own keyboard activation so commands are not executed twice, while preserving menu clicks.
NSEvent* event = [NSApp currentEvent];
if ( nil != event && event.type == NSEventTypeKeyDown )
return;
auto found = _itemMap.find( sender ); auto found = _itemMap.find( sender );
if ( found != _itemMap.end() && nullptr != found->second ) if ( found != _itemMap.end() && nullptr != found->second )
found->second->activate(); found->second->activate();
+4 -2
View File
@@ -84,7 +84,8 @@ UTEST( FontRendering, glyphAdvanceDoesNotCreateTexturePages ) {
UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) {
UIApplication app( UIApplication app(
WindowSettings( 360, 220, "eepp - Subpixel Text Test", WindowStyle::Default, WindowSettings( 360, 220, "eepp - Subpixel Text Test", WindowStyle::Default,
WindowBackend::Default, 32 ), WindowBackend::Default, 32, std::string(), 1, EE_SCREEN_KEYBOARD_ENABLED,
true ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
ResourceScope& scope = *app.getUI()->getResourceScope(); ResourceScope& scope = *app.getUI()->getResourceScope();
FontTrueTypePtr font = FontTrueType::New( "SubpixelText-Regular", scope ); FontTrueTypePtr font = FontTrueType::New( "SubpixelText-Regular", scope );
@@ -228,7 +229,8 @@ UTEST( FontRendering, subpixelCoverageAllRenderers ) {
UTEST( FontRendering, scaledSubpixelGlyphAtlas ) { UTEST( FontRendering, scaledSubpixelGlyphAtlas ) {
UIApplication app( UIApplication app(
WindowSettings( 256, 64, "eepp - Scaled Subpixel Glyph Atlas", VisualTestWindowStyle, WindowSettings( 256, 64, "eepp - Scaled Subpixel Glyph Atlas", VisualTestWindowStyle,
WindowBackend::Default, 32 ), WindowBackend::Default, 32, std::string(), 1, EE_SCREEN_KEYBOARD_ENABLED,
true ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
ResourceScope& scope = *app.getUI()->getResourceScope(); ResourceScope& scope = *app.getUI()->getResourceScope();
FontTrueTypePtr font = FontTrueType::New( "ScaledSubpixelNonicons", scope ); FontTrueTypePtr font = FontTrueType::New( "ScaledSubpixelNonicons", scope );
@@ -70,9 +70,13 @@ UTEST( SystemFontResolver, workerWarmUp ) {
warmUpThread.wait(); warmUpThread.wait();
EXPECT_FALSE( resolver->isLoading() ); EXPECT_FALSE( resolver->isLoading() );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \ #if EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \ EE_PLATFORM == EE_PLATFORM_IOS || EE_PLATFORM == EE_PLATFORM_LINUX || \
EE_PLATFORM == EE_PLATFORM_IOS || EE_PLATFORM == EE_PLATFORM_HAIKU EE_PLATFORM == EE_PLATFORM_BSD || EE_PLATFORM == EE_PLATFORM_HAIKU
EXPECT_FALSE( resolver->isFontListPopulated() );
EXPECT_FALSE( resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false )
.path.empty() );
#elif EE_PLATFORM == EE_PLATFORM_ANDROID
EXPECT_FALSE( resolver->enumerate().empty() ); EXPECT_FALSE( resolver->enumerate().empty() );
#endif #endif
@@ -80,6 +84,40 @@ UTEST( SystemFontResolver, workerWarmUp ) {
SystemFontResolver::destroySingleton(); SystemFontResolver::destroySingleton();
} }
#if EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_IOS || EE_PLATFORM == EE_PLATFORM_LINUX || \
EE_PLATFORM == EE_PLATFORM_BSD || EE_PLATFORM == EE_PLATFORM_HAIKU
UTEST( SystemFontResolver, nativeRenderingDoesNotEnumerate ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
resolver->invalidateCache();
resolver->warmUp();
EXPECT_FALSE( resolver->isFontListPopulated() );
FontDesc sans = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
EXPECT_FALSE( sans.path.empty() );
FontQuery query;
query.family = sans.family;
EXPECT_FALSE( resolver->resolve( query ).path.empty() );
EXPECT_FALSE( resolver->resolveGeneric( GenericFamily::Monospace, FontWeight::Normal, false )
.path.empty() );
#if EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_IOS
EXPECT_FALSE(
resolver->getFallbackForCodepoint( 0x65E5, FontWeight::Normal, false ).path.empty() );
#else
resolver->getFallbackForCodepoint( 0x65E5, FontWeight::Normal, false );
#endif
EXPECT_FALSE( resolver->isFontListPopulated() );
EXPECT_FALSE( resolver->enumerate().empty() );
EXPECT_TRUE( resolver->isFontListPopulated() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
#endif
UTEST( SystemFontResolver, fallbackWaitsForConcurrentWarmUp ) { UTEST( SystemFontResolver, fallbackWaitsForConcurrentWarmUp ) {
SystemFontResolver::setEnabled( true ); SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance(); auto* resolver = SystemFontResolver::instance();