fix(ui): preserve CSS order for deferred stylesheets

Reserve stylesheet source positions when link elements are encountered
  instead of assigning them when asynchronous loading completes.

  Track the stylesheet and rule ordinals explicitly so deferred external
  styles retain their document order relative to later inline styles.
  Preserve this ordering when extracting marked stylesheet rules.

  Add an SS64 regression test covering heading and button styles after a
  deferred stylesheet finishes loading.
This commit is contained in:
Martín Lucas Golini
2026-07-30 17:52:27 -03:00
parent 660395ee4d
commit 943103869a
6 changed files with 136 additions and 29 deletions
+27 -10
View File
@@ -120,21 +120,25 @@ void StyleSheet::setSelectorSpecificity( const Int64& specificity ) {
StyleSheet StyleSheet::getAllWithMarker( const Uint32& marker ) const {
StyleSheet style;
std::vector<std::shared_ptr<StyleSheetStyle>> hits;
for ( auto node : mNodes ) {
if ( node->getMarker() == marker )
style.addStyle( node );
if ( node->getMarker() == marker ) {
const SourcePosition& position = mStyleSourceOrder.at( node.get() );
style.addStyle( node, position.styleSheet, position.rule );
}
}
style.mNextStyleSourceOrder = mNextStyleSourceOrder;
return style;
}
StyleSheet StyleSheet::getAllWithMarkers() const {
StyleSheet style;
std::vector<std::shared_ptr<StyleSheetStyle>> hits;
for ( auto node : mNodes ) {
if ( node->getMarker() != 0 )
style.addStyle( node );
if ( node->getMarker() != 0 ) {
const SourcePosition& position = mStyleSourceOrder.at( node.get() );
style.addStyle( node, position.styleSheet, position.rule );
}
}
style.mNextStyleSourceOrder = mNextStyleSourceOrder;
return style;
}
@@ -225,14 +229,23 @@ bool StyleSheet::addStyleToNodeIndex( StyleSheetStyle* style ) {
}
void StyleSheet::addStyle( std::shared_ptr<StyleSheetStyle> node ) {
addStyle( std::move( node ), reserveSourceOrder(), 0 );
}
void StyleSheet::addStyle( std::shared_ptr<StyleSheetStyle> node, SourceOrder sourceOrder,
Uint32 ruleOrder ) {
if ( addStyleToNodeIndex( node.get() ) ) {
mStyleSourceOrder[node.get()] = mNextStyleSourceOrder++;
mStyleSourceOrder[node.get()] = { sourceOrder, ruleOrder };
mNodes.push_back( node );
}
addMediaQueryList( node->getMediaQueryList() );
mVersion++;
}
StyleSheet::SourceOrder StyleSheet::reserveSourceOrder() {
return mNextStyleSourceOrder++;
}
bool StyleSheet::isEmpty() const {
return mNodes.empty();
}
@@ -259,9 +272,13 @@ std::string StyleSheet::print() {
}
void StyleSheet::combineStyleSheet( const StyleSheet& styleSheet ) {
for ( auto& style : styleSheet.getStyles() ) {
addStyle( style );
}
combineStyleSheet( styleSheet, reserveSourceOrder() );
}
void StyleSheet::combineStyleSheet( const StyleSheet& styleSheet, SourceOrder sourceOrder ) {
Uint32 ruleOrder = 0;
for ( auto& style : styleSheet.getStyles() )
addStyle( style, sourceOrder, ruleOrder++ );
addKeyframes( styleSheet.getKeyframes() );
}
+23 -14
View File
@@ -552,7 +552,8 @@ std::vector<UIWidget*> UISceneNode::loadNode( pugi::xml_node node, Node* parent,
if ( !href.empty() &&
( String::iequals( type.value(), "text/css" ) ||
String::icontains( std::string_view{ rel.value() }, "stylesheet" ) ) ) {
loadCSS( href.as_string(), Milliseconds( defer.as_int() ) );
loadCSS( href.as_string(), Milliseconds( defer.as_int() ),
mStyleSheet.reserveSourceOrder() );
}
continue;
} else if ( String::iequals( widget.name(), "meta" ) ) {
@@ -703,8 +704,10 @@ void UISceneNode::updateStyleSheet( bool forceReloadStyle ) {
}
void UISceneNode::combineStyleSheet( const CSS::StyleSheet& styleSheet, bool forceReloadStyle,
URI baseURI ) {
mStyleSheet.combineStyleSheet( styleSheet );
URI baseURI,
std::optional<CSS::StyleSheet::SourceOrder> sourceOrder ) {
mStyleSheet.combineStyleSheet( styleSheet,
sourceOrder ? *sourceOrder : mStyleSheet.reserveSourceOrder() );
processStyleSheetAtRules( styleSheet, baseURI );
@@ -719,14 +722,15 @@ void UISceneNode::combineStyleSheet( const CSS::StyleSheet& styleSheet, bool for
}
void UISceneNode::combineStyleSheet( const std::string& inlineStyleSheet, bool forceReloadStyle,
const Uint32& marker, URI baseURI ) {
const Uint32& marker, URI baseURI,
std::optional<CSS::StyleSheet::SourceOrder> sourceOrder ) {
CSS::StyleSheetParser parser;
parser.setBaseURI( baseURI );
if ( parser.loadFromString( inlineStyleSheet ) ) {
parser.getStyleSheet().setMarker( marker );
resolveStyleSheetRelativeURLs( parser.getStyleSheet(), baseURI.empty() ? mURI : baseURI );
combineStyleSheet( parser.getStyleSheet(), forceReloadStyle, baseURI );
combineStyleSheet( parser.getStyleSheet(), forceReloadStyle, baseURI, sourceOrder );
}
}
@@ -1857,7 +1861,8 @@ URI UISceneNode::solveRelativePath( URI uri, URI baseURI ) {
return base;
}
void UISceneNode::loadCSS( URI uri, std::optional<Time> defer ) {
void UISceneNode::loadCSS( URI uri, std::optional<Time> defer,
CSS::StyleSheet::SourceOrder sourceOrder ) {
uri = solveRelativePath( uri );
std::string url = uri.toString();
Log::debug( "UISceneNode::loadCSS: %s", url );
@@ -1869,7 +1874,7 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer ) {
Uint64 resourceGeneration =
resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0;
URI baseURL = getURIFromURL( url );
mThreadPool->run( [resourceState, resourceGeneration, uri, url, defer,
mThreadPool->run( [resourceState, resourceGeneration, uri, url, defer, sourceOrder,
baseURL = std::move( baseURL )] {
Clock c;
std::string filePath( uri.getFSPath() );
@@ -1883,9 +1888,10 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer ) {
delay = Time::Zero;
UISceneNode::runAsyncResourceOnMainThread(
resourceState, resourceGeneration,
[url, baseURL,
[url, baseURL, sourceOrder,
parser = std::move( parser )]( UISceneNode* scene ) mutable {
scene->combineStyleSheet( parser.getStyleSheet(), true, baseURL );
scene->combineStyleSheet( parser.getStyleSheet(), true, baseURL,
sourceOrder );
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
},
delay );
@@ -1896,7 +1902,8 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer ) {
std::string filePath( uri.getFSPath() );
std::string css;
if ( FileSystem::fileExists( filePath ) && FileSystem::fileGet( filePath, css ) ) {
combineStyleSheet( css, true, String::hash( url ), getURIFromURL( url ) );
combineStyleSheet( css, true, String::hash( url ), getURIFromURL( url ),
sourceOrder );
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
}
}
@@ -1910,7 +1917,7 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer ) {
request.kind = WebResourceKind::StyleSheet;
request.timeout = Seconds( 5 );
requestWebResource( std::move( request ), [resourceState, resourceGeneration, url,
baseURL = std::move( baseURL )](
sourceOrder, baseURL = std::move( baseURL )](
const WebResourceResult& result ) {
if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) )
return;
@@ -1918,8 +1925,10 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer ) {
std::string css( *result.data );
UISceneNode::runAsyncResourceOnMainThread(
resourceState, resourceGeneration,
[css = std::move( css ), url, baseURL]( UISceneNode* scene ) mutable {
scene->combineStyleSheet( css, true, String::hash( url ), baseURL );
[css = std::move( css ), url, baseURL,
sourceOrder]( UISceneNode* scene ) mutable {
scene->combineStyleSheet( css, true, String::hash( url ), baseURL,
sourceOrder );
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
} );
} else {
@@ -1931,7 +1940,7 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer ) {
CSS::StyleSheetParser parser;
if ( parser.loadFromStream( *stream ) ) {
parser.getStyleSheet().setMarker( String::hash( url ) );
combineStyleSheet( parser.getStyleSheet() );
combineStyleSheet( parser.getStyleSheet(), true, {}, sourceOrder );
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
}
} else {
@@ -703,6 +703,50 @@ UTEST( UIHTMLFloat, ss64NarrowViewportFloatsDoNotOverlapAtPixelDensity1 ) {
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, ss64DeferredStyleSheetPreservesDocumentSourceOrder ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setThreadPool( ThreadPool::createShared( 1 ) );
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<html>
<head>
<link rel="stylesheet" href="ss64_deferred_cascade.css" defer="25" />
<style>
h1 { font-size: 1rem; }
li a[href="../bash/"] { background-color: #FFCC33; }
</style>
</head>
<body>
<div id="external-css-loaded"></div>
<h1 id="heading">Command line reference.</h1>
<ul><li><a id="linux" href="../bash/">Linux</a></li></ul>
</body>
</html>
)html" ) );
auto* heading = sceneNode->getRoot()->find( "heading" )->asType<UIRichText>();
auto* linux = sceneNode->getRoot()->find( "linux" )->asType<UIRichText>();
auto* loaded = sceneNode->getRoot()->find( "external-css-loaded" )->asType<UIWidget>();
ASSERT_TRUE( heading != nullptr );
ASSERT_TRUE( linux != nullptr );
ASSERT_TRUE( loaded != nullptr );
const Uint32 inlineHeadingFontSize = heading->getFontSize();
const Color inlineLinuxBackground = linux->getBackgroundColor();
for ( int i = 0; i < 500 && loaded->getPixelsSize().getHeight() < 36.f; ++i ) {
SceneManager::instance()->update();
Sys::sleep( Milliseconds( 1 ) );
}
ASSERT_NEAR( loaded->getPixelsSize().getHeight(), 37.f, 1.f );
EXPECT_EQ( heading->getFontSize(), inlineHeadingFontSize );
EXPECT_TRUE( linux->getBackgroundColor() == inlineLinuxBackground );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, autoHorizontalMarginsCenterBlockInsideFloat ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();