From d3ed96919a75eef755e663eb4772eb3d9852ae76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 30 Jul 2026 19:08:49 -0300 Subject: [PATCH] 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. --- .../assets/html/ss64_deferred_cascade.css | 11 +++ include/eepp/ui/uihtmlwidget.hpp | 3 + src/eepp/ui/uihtmlwidget.cpp | 49 ++++++++++ src/tests/unit_tests/uihtml_float_tests.cpp | 98 +++++++++++++++++++ 4 files changed, 161 insertions(+) diff --git a/bin/unit_tests/assets/html/ss64_deferred_cascade.css b/bin/unit_tests/assets/html/ss64_deferred_cascade.css index 8fdbd76a7..40d66f5a7 100644 --- a/bin/unit_tests/assets/html/ss64_deferred_cascade.css +++ b/bin/unit_tests/assets/html/ss64_deferred_cascade.css @@ -6,6 +6,17 @@ li a[href="../bash/"] { background-color: #F8F9FA; } +.tbtn { + height: 40px; + line-height: 40px; + width: 140px; +} + +.tbtn a { + background-color: #F8F9FA; + display: block; +} + #external-css-loaded { height: 37px; } diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index 8282aee0e..c91fd648f 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -310,6 +310,8 @@ class EE_API UIHTMLWidget : public UILayout { virtual void onParentChange(); + virtual void onParentSizeChange( const Vector2f& sizeChange ); + virtual void onPositionChange(); UIWidget* getContainingBlock(); @@ -368,6 +370,7 @@ class EE_API UIHTMLWidget : public UILayout { bool mOverflowCreatesBlockFormattingContext{ false }; bool mNeedsOrderSort{ false }; bool mNeedsZIndexSort{ false }; + Uint8 mPercentageMargins{ 0 }; UILayouter* mLayouter{ nullptr }; UIHTMLWidgetFlexState* mFlexState{ nullptr }; UIHTMLWidgetGridState* mGridState{ nullptr }; diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index 58e80e55c..a748386c3 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -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 ) { diff --git a/src/tests/unit_tests/uihtml_float_tests.cpp b/src/tests/unit_tests/uihtml_float_tests.cpp index 937a176d7..304a1a044 100644 --- a/src/tests/unit_tests/uihtml_float_tests.cpp +++ b/src/tests/unit_tests/uihtml_float_tests.cpp @@ -8,10 +8,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -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( + + + + + + +
+

Command line reference.

+
+
+ + + + )html" ), + webView->getDocumentContainer(), + String::hash( "ss64-deferred-footer" ) ); + webView->refreshDocumentLayout(); + + auto* loaded = documentScene->getRoot()->find( "external-css-loaded" )->asType(); + auto* heading = documentScene->getRoot()->find( "heading" )->asType(); + auto* footer = documentScene->getRoot()->find( "footer" )->asType(); + 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" ) ); + + auto* body = sceneNode->getRoot()->querySelector( "body" )->asType(); + auto* target = sceneNode->getRoot()->find( "target" )->asType(); + 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();