Fix percentage in font-size for UIWebView.

Unload fonts from scene in UIWebView.
This commit is contained in:
Martín Lucas Golini
2026-07-02 00:06:02 -03:00
parent f23b355ecd
commit 772742cbbb
8 changed files with 162 additions and 37 deletions
@@ -168,7 +168,11 @@ application UISceneNode
`UISceneNode::overFind()` compatibility override. `UIRoot` keeps its layout/self-hit `UISceneNode::overFind()` compatibility override. `UIRoot` keeps its layout/self-hit
bounds viewport-sized, but embedded document scenes can ask it to traverse child bounds viewport-sized, but embedded document scenes can ask it to traverse child
hit testing through the measured document extent. hit testing through the measured document extent.
- Basic author `@font-face` isolation is implemented and covered by UIWebView tests. - Author `@font-face` isolation and cleanup are implemented. Scene-local aliases resolve before
global font fallback; WebView navigation clears the document scene's previous author aliases and
internally registered font resources; document scene destruction removes any remaining scene-owned
author fonts. Tests cover sibling-scene isolation, navigation cleanup, and WebView destruction
cleanup.
- Tests cover the new topology, viewport-vs-extent behavior, scrolling, two-scene - Tests cover the new topology, viewport-vs-extent behavior, scrolling, two-scene
style isolation, navigation supersession, and a resize metric regression that guards style isolation, navigation supersession, and a resize metric regression that guards
against no-op queued viewport churn rebuilding RichText. They also cover document against no-op queued viewport churn rebuilding RichText. They also cover document
@@ -176,9 +180,6 @@ application UISceneNode
### Pending / Follow-Up ### Pending / Follow-Up
- **Author `@font-face` cleanup audit** should verify navigation/destruction cleanup
for scene-local aliases and loaded font resources. The basic scene-local isolation
path is implemented and tested.
- **Subresource lifetime coverage** should be completed for every async path described - **Subresource lifetime coverage** should be completed for every async path described
in Phase 6, including deferred CSS, fonts, images, redirects, cookies, and destruction. in Phase 6, including deferred CSS, fonts, images, redirects, cookies, and destruction.
- **Example and documentation integration** should be completed after the code shape - **Example and documentation integration** should be completed after the code shape
@@ -385,7 +386,7 @@ Steps:
1. Add a scene-local font-face alias registry keyed by CSS family, style, and weight. 1. Add a scene-local font-face alias registry keyed by CSS family, style, and weight.
2. Register loaded author fonts under scene-unique internal names. 2. Register loaded author fonts under scene-unique internal names.
3. Resolve author font aliases before global `FontManager` fallback. 3. Resolve author font aliases before global `FontManager` fallback.
4. Add `clearAuthorFontFaces()` and call it during navigation. 4. Add `clearFontFaces()` and call it during navigation.
5. Remove only this scene's internally registered author fonts during scene destruction. 5. Remove only this scene's internally registered author fonts during scene destruction.
6. Mark document extent dirty after a font load can affect metrics. 6. Mark document extent dirty after a font load can affect metrics.
@@ -2,8 +2,8 @@
> Status: IMPLEMENTED WITH FOLLOW-UPS - the owned document scene, real scroll-target > Status: IMPLEMENTED WITH FOLLOW-UPS - the owned document scene, real scroll-target
> layout widget, viewport/extent split, root-scoped hit-test traversal, and focused > layout widget, viewport/extent split, root-scoped hit-test traversal, and focused
> UIWebView coverage are implemented. Remaining work is cleanup/audit coverage for > UIWebView coverage are implemented. Remaining work is broader async subresource
> async subresources, examples/docs, and fixed/sticky acceptance tests. > coverage, examples/docs, and fixed/sticky acceptance tests.
## Goal ## Goal
@@ -453,7 +453,7 @@ Steps:
3. Register author fonts under an internal scene-unique resource name if `FontManager` registration 3. Register author fonts under an internal scene-unique resource name if `FontManager` registration
remains required, while preserving the author-visible family only in the scene-local alias. remains required, while preserving the author-visible family only in the scene-local alias.
4. Keep generic/system fonts and explicitly shared application defaults as global fallbacks. 4. Keep generic/system fonts and explicitly shared application defaults as global fallbacks.
5. Add an explicit `clearDocumentFontFaces()` / `clearAuthorFontFaces()` operation used during 5. Add an explicit `clearFontFaces()` operation used during
navigation before new document CSS is loaded. It removes only this scene's internally registered navigation before new document CSS is loaded. It removes only this scene's internally registered
author fonts and clears aliases; it must not remove application/system fonts or sibling-document author fonts and clears aliases; it must not remove application/system fonts or sibling-document
author fonts. author fonts.
+2
View File
@@ -822,6 +822,8 @@ class EE_API UISceneNode : public SceneNode {
Font* getFontFromNamesList( std::string_view names, Uint32 fontStyle = 0, Font* getFontFromNamesList( std::string_view names, Uint32 fontStyle = 0,
FontWeight weight = FontWeight::Normal ) const; FontWeight weight = FontWeight::Normal ) const;
void clearFontFaces();
Font* reevaluateFontStyle( Font* currentFont, Uint32 fontStyle, Font* reevaluateFontStyle( Font* currentFont, Uint32 fontStyle,
FontWeight weight = FontWeight::Normal ) const; FontWeight weight = FontWeight::Normal ) const;
+25 -18
View File
@@ -1702,9 +1702,29 @@ Float UINode::lengthFromValue( const StyleSheetProperty& property,
if ( property.getPropertyDefinition() && if ( property.getPropertyDefinition() &&
property.getPropertyDefinition()->getPropertyId() == PropertyId::FontSize ) { property.getPropertyDefinition()->getPropertyId() == PropertyId::FontSize ) {
StyleSheetLength length( property.value() ); StyleSheetLength length( property.value() );
auto parentFontSize = [this]() {
Float fontSize = 12.f * PixelDensity::getPixelDensity();
Node* parentNode = getParent();
while ( parentNode ) {
if ( parentNode->isWidget() ) {
fontSize = getAbsoluteFontSize( parentNode->asType<UIWidget>() );
break;
}
parentNode = parentNode->getParent();
}
return fontSize;
};
auto resolveFontRelativeLength = [this, &parentFontSize]( const StyleSheetLength& len ) {
const Float parentSize = parentFontSize();
Font* font = nullptr;
if ( getUISceneNode() && getUISceneNode()->getUIThemeManager() )
font = getUISceneNode()->getUIThemeManager()->getDefaultFont();
return len.asPixels( parentSize, Sizef::Zero, getSceneNode()->getDPI(), parentSize,
parentSize, font );
};
if ( length.getUnit() == StyleSheetLength::Unit::Percentage ) { if ( length.getUnit() == StyleSheetLength::Unit::Percentage ) {
length.setValue( length.getValue() / 100.f, StyleSheetLength::Unit::Em ); return resolveFontRelativeLength( length );
return convertLength( length, 0 );
} }
static constexpr std::string_view FontSizeNames[] = { static constexpr std::string_view FontSizeNames[] = {
@@ -1738,6 +1758,8 @@ Float UINode::lengthFromValue( const StyleSheetProperty& property,
} else if ( keyword == "larger" ) { } else if ( keyword == "larger" ) {
res.setValue( 1.2f, StyleSheetLength::Unit::Em ); res.setValue( 1.2f, StyleSheetLength::Unit::Em );
} }
if ( res.getUnit() == StyleSheetLength::Unit::Em )
return resolveFontRelativeLength( res );
return convertLength( res, 0 ); return convertLength( res, 0 );
} else if ( property.getValue() == "inherit" ) { } else if ( property.getValue() == "inherit" ) {
Node* parentNode = getParent(); Node* parentNode = getParent();
@@ -1757,22 +1779,7 @@ Float UINode::lengthFromValue( const StyleSheetProperty& property,
length.getUnit() != StyleSheetLength::Unit::Ch ) length.getUnit() != StyleSheetLength::Unit::Ch )
return convertLength( length, 0 ); return convertLength( length, 0 );
Float parentFontSize = 12.f * PixelDensity::getPixelDensity(); return resolveFontRelativeLength( length );
Node* parentNode = getParent();
while ( parentNode ) {
if ( parentNode->isWidget() ) {
parentFontSize = getAbsoluteFontSize( parentNode->asType<UIWidget>() );
break;
}
parentNode = parentNode->getParent();
}
Font* font = nullptr;
if ( getUISceneNode() && getUISceneNode()->getUIThemeManager() )
font = getUISceneNode()->getUIThemeManager()->getDefaultFont();
return length.asPixels( 0, Sizef::Zero, getSceneNode()->getDPI(), parentFontSize,
parentFontSize, font );
} }
return lengthFromValue( property.getValue(), return lengthFromValue( property.getValue(),
property.getPropertyDefinition()->getRelativeTarget(), defaultValue, property.getPropertyDefinition()->getRelativeTarget(), defaultValue,
+16 -4
View File
@@ -111,13 +111,11 @@ UISceneNode::~UISceneNode() {
mAsyncResourceLoadState->generation++; mAsyncResourceLoadState->generation++;
} }
clearFontFaces();
eeSAFE_DELETE( mUIThemeManager ); eeSAFE_DELETE( mUIThemeManager );
eeSAFE_DELETE( mUIIconThemeManager ); eeSAFE_DELETE( mUIIconThemeManager );
for ( auto& font : mFontFaces ) {
FontManager::instance()->remove( font );
}
// UISceneNode can now destroy the ThreadPool shared to him. If that's the case, // UISceneNode can now destroy the ThreadPool shared to him. If that's the case,
// We need to ensure that the children are destroyed before the thread pool, // We need to ensure that the children are destroyed before the thread pool,
// since its children could be consuming it and need to uninitialize gracefully. // since its children could be consuming it and need to uninitialize gracefully.
@@ -1937,6 +1935,20 @@ Font* UISceneNode::getFontFromNamesList( std::string_view names, Uint32 fontStyl
return font; return font;
} }
void UISceneNode::clearFontFaces() {
if ( mFontFaces.empty() && mFontFaceAliases.empty() )
return;
mFontFaceAliases.clear();
if ( mRoot )
mRoot->reloadFontFamily();
for ( auto& font : mFontFaces )
FontManager::instance()->remove( font );
mFontFaces.clear();
}
Font* UISceneNode::reevaluateFontStyle( Font* currentFont, Uint32 fontStyle, Font* UISceneNode::reevaluateFontStyle( Font* currentFont, Uint32 fontStyle,
FontWeight weight ) const { FontWeight weight ) const {
if ( !currentFont || !SystemFontResolver::isEnabled() ) if ( !currentFont || !SystemFontResolver::isEnabled() )
+1
View File
@@ -375,6 +375,7 @@ void UIWebView::loadDocumentData( URI url, std::string data, Uint64 generation )
self->getHorizontalScrollBar()->setValue( 0 ); self->getHorizontalScrollBar()->setValue( 0 );
static_cast<UIWebViewDocumentContainer*>( self->mDocContainer )->clearDocumentChildren(); static_cast<UIWebViewDocumentContainer*>( self->mDocContainer )->clearDocumentChildren();
ui->invalidateAsyncResourceLoads(); ui->invalidateAsyncResourceLoads();
ui->clearFontFaces();
ui->getStyleSheet().removeAllWithoutMarker( self->mStyleSheetDefaultMarker ); ui->getStyleSheet().removeAllWithoutMarker( self->mStyleSheetDefaultMarker );
ui->setURIFromURL( url ); ui->setURIFromURL( url );
@@ -153,7 +153,7 @@ UTEST( CSSInheritance, ComputedFontSizePercentageAndRem ) {
UIWidget* targetSpan = root->querySelector( "#targetspan" ); UIWidget* targetSpan = root->querySelector( "#targetspan" );
EXPECT_TRUE( targetSpan != nullptr ); EXPECT_TRUE( targetSpan != nullptr );
EXPECT_NEAR( 18u * scale, targetSpan->asType<UITextSpan>()->getFontSize(), 1.f ); EXPECT_NEAR( 30u * scale, targetSpan->asType<UITextSpan>()->getFontSize(), 1.f );
} }
} }
+108 -6
View File
@@ -203,10 +203,12 @@ UTEST( UIWebView, FontSizeEmDoesNotCompoundOnViewportRelayout ) {
html, body { margin: 0; padding: 0; } html, body { margin: 0; padding: 0; }
body { font-size: 16px; } body { font-size: 16px; }
h1 { font-size: 2.5em; margin: 0.5em 0; } h1 { font-size: 2.5em; margin: 0.5em 0; }
h2 { font-size: 250%; margin: 0.5em 0; }
</style> </style>
</head> </head>
<body> <body>
<h1><span id="title-text">Title</span></h1> <h1><span id="title-em">Title em</span></h1>
<h2><span id="title-percent">Title percent</span></h2>
<div style="height: 900px"></div> <div style="height: 900px"></div>
</body> </body>
</html> </html>
@@ -224,16 +226,20 @@ UTEST( UIWebView, FontSizeEmDoesNotCompoundOnViewportRelayout ) {
}; };
pump(); pump();
Node* title = documentScene->getRoot()->find( "title-text" ); Node* titleEm = documentScene->getRoot()->find( "title-em" );
ASSERT_TRUE( title != nullptr && title->isType( UI_TYPE_TEXTSPAN ) ); Node* titlePercent = documentScene->getRoot()->find( "title-percent" );
EXPECT_NEAR( title->asType<UITextSpan>()->getFontSize(), 40.f, 1.f ); ASSERT_TRUE( titleEm != nullptr && titleEm->isType( UI_TYPE_TEXTSPAN ) );
ASSERT_TRUE( titlePercent != nullptr && titlePercent->isType( UI_TYPE_TEXTSPAN ) );
EXPECT_NEAR( titleEm->asType<UITextSpan>()->getFontSize(), 40.f, 1.f );
EXPECT_NEAR( titlePercent->asType<UITextSpan>()->getFontSize(), 40.f, 1.f );
for ( int i = 0; i < 4; i++ ) { for ( int i = 0; i < 4; i++ ) {
webView->setPixelsSize( 520 + i * 20, 320 + i * 10 ); webView->setPixelsSize( 520 + i * 20, 320 + i * 10 );
pump(); pump();
webView->setPixelsSize( 420, 260 ); webView->setPixelsSize( 420, 260 );
pump(); pump();
EXPECT_NEAR( title->asType<UITextSpan>()->getFontSize(), 40.f, 1.f ); EXPECT_NEAR( titleEm->asType<UITextSpan>()->getFontSize(), 40.f, 1.f );
EXPECT_NEAR( titlePercent->asType<UITextSpan>()->getFontSize(), 40.f, 1.f );
} }
Engine::destroySingleton(); Engine::destroySingleton();
@@ -1479,6 +1485,8 @@ UTEST( UIWebView, DocumentScenesIsolateAuthorFontFaces ) {
const std::string processPath( Sys::getProcessPath() ); const std::string processPath( Sys::getProcessPath() );
const std::string pathA = Sys::getTempPath() + "eepp_uiwebview_font_doc_a.html"; const std::string pathA = Sys::getTempPath() + "eepp_uiwebview_font_doc_a.html";
const std::string pathB = Sys::getTempPath() + "eepp_uiwebview_font_doc_b.html"; const std::string pathB = Sys::getTempPath() + "eepp_uiwebview_font_doc_b.html";
const std::string pathAWithoutFont =
Sys::getTempPath() + "eepp_uiwebview_font_doc_a_without_font.html";
const std::string pathA2 = Sys::getTempPath() + "eepp_uiwebview_font_doc_a2.html"; const std::string pathA2 = Sys::getTempPath() + "eepp_uiwebview_font_doc_a2.html";
FileSystem::fileWrite( pathA, FileSystem::fileWrite( pathA,
"<html><head><style>" "<html><head><style>"
@@ -1494,6 +1502,11 @@ UTEST( UIWebView, DocumentScenesIsolateAuthorFontFaces ) {
"../assets/fonts/DejaVuSansMono.ttf'); }" "../assets/fonts/DejaVuSansMono.ttf'); }"
"#target-b { font-family: 'SharedDocFace'; }" "#target-b { font-family: 'SharedDocFace'; }"
"</style></head><body><span id='target-b'>B</span></body></html>" ); "</style></head><body><span id='target-b'>B</span></body></html>" );
FileSystem::fileWrite( pathAWithoutFont,
"<html><head><style>"
"#target-a-empty { font-family: 'SharedDocFace'; }"
"</style></head><body><span id='target-a-empty'>A empty</span></body>"
"</html>" );
FileSystem::fileWrite( FileSystem::fileWrite(
pathA2, "<html><head><style>" pathA2, "<html><head><style>"
"@font-face { font-family: 'SharedDocFace'; src: url('file://" + "@font-face { font-family: 'SharedDocFace'; src: url('file://" +
@@ -1525,6 +1538,16 @@ UTEST( UIWebView, DocumentScenesIsolateAuthorFontFaces ) {
EXPECT_TRUE( fontB->loaded() ); EXPECT_TRUE( fontB->loaded() );
EXPECT_NE( fontA, fontB ); EXPECT_NE( fontA, fontB );
EXPECT_EQ( nullptr, FontManager::instance()->getByName( "SharedDocFace" ) ); EXPECT_EQ( nullptr, FontManager::instance()->getByName( "SharedDocFace" ) );
const std::string fontAResourceName = fontA->getName();
EXPECT_EQ( fontA, FontManager::instance()->getByName( fontAResourceName ) );
webViewA->loadURI( URI( "file://" + pathAWithoutFont ) );
pump();
auto targetAWithoutFont = docA->getRoot()->find( "target-a-empty" );
ASSERT_TRUE( targetAWithoutFont != nullptr );
EXPECT_EQ( nullptr, docA->getFontFromNamesList( "SharedDocFace" ) );
EXPECT_EQ( nullptr, FontManager::instance()->getByName( fontAResourceName ) );
webViewA->loadURI( URI( "file://" + pathA2 ) ); webViewA->loadURI( URI( "file://" + pathA2 ) );
pump(); pump();
@@ -1533,13 +1556,92 @@ UTEST( UIWebView, DocumentScenesIsolateAuthorFontFaces ) {
Font* stableFontB = docB->getFontFromNamesList( "SharedDocFace" ); Font* stableFontB = docB->getFontFromNamesList( "SharedDocFace" );
ASSERT_TRUE( reloadedFontA != nullptr ); ASSERT_TRUE( reloadedFontA != nullptr );
ASSERT_TRUE( stableFontB != nullptr ); ASSERT_TRUE( stableFontB != nullptr );
EXPECT_NE( reloadedFontA, fontA );
EXPECT_EQ( stableFontB, fontB ); EXPECT_EQ( stableFontB, fontB );
EXPECT_NE( reloadedFontA, stableFontB ); EXPECT_NE( reloadedFontA, stableFontB );
Engine::destroySingleton(); Engine::destroySingleton();
} }
UTEST( UIWebView, DocumentSceneAuthorFontFacesCleanUpOnDestruction ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 800, 600, "UIWebView Font Face Cleanup Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
ASSERT_TRUE( font != nullptr && font->loaded() );
FontFamily::loadFromRegular( font );
UISceneNode* sceneNode = UISceneNode::New();
SceneManager::instance()->add( sceneNode );
sceneNode->getUIThemeManager()->setDefaultFont( font );
UIWebView* webViewA = UIWebView::New();
webViewA->setParent( sceneNode->getRoot() );
webViewA->setPixelsSize( 300, 200 );
webViewA->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIWebView* webViewB = UIWebView::New();
webViewB->setParent( sceneNode->getRoot() );
webViewB->setPixelsSize( 300, 200 );
webViewB->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
const std::string processPath( Sys::getProcessPath() );
const std::string pathA = Sys::getTempPath() + "eepp_uiwebview_font_doc_destroy_a.html";
const std::string pathB = Sys::getTempPath() + "eepp_uiwebview_font_doc_destroy_b.html";
FileSystem::fileWrite( pathA,
"<html><head><style>"
"@font-face { font-family: 'DestroyDocFace'; src: url('file://" +
processPath +
"../assets/fonts/DejaVuSansMono.ttf'); }"
"#target-a { font-family: 'DestroyDocFace'; }"
"</style></head><body><span id='target-a'>A</span></body></html>" );
FileSystem::fileWrite( pathB,
"<html><head><style>"
"@font-face { font-family: 'DestroyDocFace'; src: url('file://" +
processPath +
"../assets/fonts/NotoSans-Regular.ttf'); }"
"#target-b { font-family: 'DestroyDocFace'; }"
"</style></head><body><span id='target-b'>B</span></body></html>" );
webViewA->loadURI( URI( "file://" + pathA ) );
webViewB->loadURI( URI( "file://" + pathB ) );
auto pump = [&]() {
for ( int i = 0; i < 10; i++ ) {
win->getInput()->update();
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
}
};
pump();
UISceneNode* docA = webViewA->getDocumentSceneNode();
UISceneNode* docB = webViewB->getDocumentSceneNode();
ASSERT_TRUE( docA != nullptr );
ASSERT_TRUE( docB != nullptr );
Font* loadedFontA = docA->getFontFromNamesList( "DestroyDocFace" );
Font* loadedFontB = docB->getFontFromNamesList( "DestroyDocFace" );
ASSERT_TRUE( loadedFontA != nullptr );
ASSERT_TRUE( loadedFontB != nullptr );
EXPECT_TRUE( loadedFontA->loaded() );
EXPECT_TRUE( loadedFontB->loaded() );
EXPECT_NE( loadedFontA, loadedFontB );
const std::string loadedFontAName = loadedFontA->getName();
const std::string loadedFontBName = loadedFontB->getName();
EXPECT_EQ( loadedFontA, FontManager::instance()->getByName( loadedFontAName ) );
EXPECT_EQ( loadedFontB, FontManager::instance()->getByName( loadedFontBName ) );
webViewA->close();
pump();
EXPECT_EQ( nullptr, FontManager::instance()->getByName( loadedFontAName ) );
EXPECT_EQ( loadedFontB, FontManager::instance()->getByName( loadedFontBName ) );
EXPECT_EQ( loadedFontB, docB->getFontFromNamesList( "DestroyDocFace" ) );
Engine::destroySingleton();
}
UTEST( UIWebView, NewerNavigationSupersedesStartedLoad ) { UTEST( UIWebView, NewerNavigationSupersedesStartedLoad ) {
auto win = Engine::instance()->createWindow( auto win = Engine::instance()->createWindow(
WindowSettings( 800, 600, "UIWebView Stale Navigation Test", WindowStyle::Default, WindowSettings( 800, 600, "UIWebView Stale Navigation Test", WindowStyle::Default,