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
File diff suppressed because it is too large Load Diff
@@ -495,6 +495,13 @@ struct NativeMenuSource {
- (void)menuItemActivated:(NSMenuItem*)sender {
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 );
if ( found != _itemMap.end() && nullptr != found->second )
found->second->activate();
+4 -2
View File
@@ -84,7 +84,8 @@ UTEST( FontRendering, glyphAdvanceDoesNotCreateTexturePages ) {
UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) {
UIApplication app(
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 ) );
ResourceScope& scope = *app.getUI()->getResourceScope();
FontTrueTypePtr font = FontTrueType::New( "SubpixelText-Regular", scope );
@@ -228,7 +229,8 @@ UTEST( FontRendering, subpixelCoverageAllRenderers ) {
UTEST( FontRendering, scaledSubpixelGlyphAtlas ) {
UIApplication app(
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 ) );
ResourceScope& scope = *app.getUI()->getResourceScope();
FontTrueTypePtr font = FontTrueType::New( "ScaledSubpixelNonicons", scope );
@@ -70,9 +70,13 @@ UTEST( SystemFontResolver, workerWarmUp ) {
warmUpThread.wait();
EXPECT_FALSE( resolver->isLoading() );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_IOS || EE_PLATFORM == EE_PLATFORM_HAIKU
#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
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() );
#endif
@@ -80,6 +84,40 @@ UTEST( SystemFontResolver, workerWarmUp ) {
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 ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();