Fix percentage margin resolution after async HTML layout

Track percentage-based margins in UIHTMLWidget and re-resolve them when
  the containing parent acquires its final size. This prevents margins
  computed against an initial zero-width container from remaining stale
  until the viewport is resized.

  Add regressions covering percentage margin recomputation and the SS64
  footer layout after a deferred stylesheet is loaded.
This commit is contained in:
Martín Lucas Golini
2026-07-30 19:08:49 -03:00
parent 943103869a
commit d3ed96919a
4 changed files with 161 additions and 0 deletions
+49
View File
@@ -937,6 +937,33 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
mOverflowCreatesBlockFormattingContext =
!String::iequals( attribute.getValue(), "visible" );
return UILayout::applyProperty( attribute );
case PropertyId::MarginTop:
case PropertyId::MarginRight:
case PropertyId::MarginBottom:
case PropertyId::MarginLeft: {
Uint8 marginBit = 0;
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
case PropertyId::MarginTop:
marginBit = 1 << 0;
break;
case PropertyId::MarginRight:
marginBit = 1 << 1;
break;
case PropertyId::MarginBottom:
marginBit = 1 << 2;
break;
case PropertyId::MarginLeft:
marginBit = 1 << 3;
break;
default:
break;
}
if ( StyleSheetLength::isPercentage( attribute.value() ) )
mPercentageMargins |= marginBit;
else
mPercentageMargins &= ~marginBit;
return UILayout::applyProperty( attribute );
}
case PropertyId::Width:
case PropertyId::Height:
case PropertyId::PaddingLeft:
@@ -1395,6 +1422,28 @@ void UIHTMLWidget::onParentChange() {
updateScrollListeners();
}
void UIHTMLWidget::onParentSizeChange( const Vector2f& sizeChange ) {
UILayout::onParentSizeChange( sizeChange );
if ( mPercentageMargins == 0 || !getUIStyle() )
return;
static constexpr PropertyId MarginProperties[] = {
PropertyId::MarginTop,
PropertyId::MarginRight,
PropertyId::MarginBottom,
PropertyId::MarginLeft,
};
for ( Uint32 i = 0; i < 4; ++i ) {
if ( !( mPercentageMargins & ( 1 << i ) ) )
continue;
PropertyId propertyId = MarginProperties[i];
const StyleSheetProperty* property = getUIStyle()->getProperty( propertyId );
if ( property )
applyProperty( *property );
}
}
void UIHTMLWidget::onPositionChange() {
UILayout::onPositionChange();
if ( mPosition == CSSPosition::Sticky && !mIsUpdatingScroll ) {
@@ -8,10 +8,12 @@
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uisvg.hpp>
#include <eepp/ui/uitextnode.hpp>
#include <eepp/ui/uitheme.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uiwebview.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/window.hpp>
@@ -747,6 +749,102 @@ UTEST( UIHTMLFloat, ss64DeferredStyleSheetPreservesDocumentSourceOrder ) {
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, ss64DeferredStyleSheetRelayoutsFooter ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setThreadPool( ThreadPool::createShared( 1 ) );
UIWebView* webView = UIWebView::New();
webView->setParent( sceneNode->getRoot() );
webView->setPixelsSize( 1280, 650 );
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UISceneNode* documentScene = webView->getDocumentSceneNode();
ASSERT_TRUE( documentScene != nullptr );
documentScene->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
documentScene->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<html>
<head>
<link rel="stylesheet" href="ss64_deferred_cascade.css" defer="250" />
<style>
body { margin: 15px; }
h1 { clear: both; font-size: 1rem; }
.tnav { height: 42px; max-width: 700px; }
.tnav > ul { list-style: none; margin: 0; padding: 0; }
.tnav > ul > li {
display: block; float: left; margin: 0 0.15em 15px; text-align: center;
}
.footer { display: inline-block; font-size: 75%; }
#first, #second { margin-left: 6.5%; }
li a[href="../bash/"] { background-color: #FFCC33; }
</style>
</head>
<body>
<div id="external-css-loaded"></div>
<h1 id="heading">Command line reference.</h1>
<div class="tnav" id="first"><ul>
<li class="tbtn"><a href="../bash/">Linux</a></li>
<li class="tbtn"><a href="../mac/">macOS</a></li>
<li class="tbtn"><a href="../nt/">CMD</a></li>
<li class="tbtn"><a href="../ps/">PowerShell</a></li>
</ul></div><br />
<div class="tnav" id="second"><ul>
<li class="tbtn"><a href="../ascii.html">ASCII</a></li>
<li class="tbtn"><a href="../vb/">VBScript</a></li>
<li class="tbtn"><a href="../tools/">Tools</a></li>
<li class="tbtn"><a href="../pass/">Passwords</a></li>
</ul></div><br />
<p class="footer" id="footer">About/contact - Last update<br />Copyright</p>
</body>
</html>
)html" ),
webView->getDocumentContainer(),
String::hash( "ss64-deferred-footer" ) );
webView->refreshDocumentLayout();
auto* loaded = documentScene->getRoot()->find( "external-css-loaded" )->asType<UIWidget>();
auto* heading = documentScene->getRoot()->find( "heading" )->asType<UIWidget>();
auto* footer = documentScene->getRoot()->find( "footer" )->asType<UIWidget>();
ASSERT_TRUE( loaded != nullptr );
ASSERT_TRUE( heading != nullptr );
ASSERT_TRUE( footer != nullptr );
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_NEAR( footer->getPixelsPosition().x, heading->getPixelsPosition().x, 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, percentageMarginReResolvesWhenContainingBlockGetsWidth ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<html><body><div id="target" style="margin-left: 6.5%;"></div></body></html>
)html" ) );
auto* body = sceneNode->getRoot()->querySelector( "body" )->asType<UIWidget>();
auto* target = sceneNode->getRoot()->find( "target" )->asType<UIWidget>();
ASSERT_TRUE( body != nullptr );
ASSERT_TRUE( target != nullptr );
const StyleSheetProperty* marginLeft =
target->getUIStyle()->getProperty( PropertyId::MarginLeft );
ASSERT_TRUE( marginLeft != nullptr );
body->setPixelsSize( 0.f, body->getPixelsSize().getHeight() );
target->applyProperty( *marginLeft );
EXPECT_NEAR( target->getLayoutPixelsMargin().Left, 0.f, 0.01f );
body->setPixelsSize( 1000.f, body->getPixelsSize().getHeight() );
EXPECT_NEAR( target->getLayoutPixelsMargin().Left, 65.f, 0.01f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, autoHorizontalMarginsCenterBlockInsideFloat ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();