From 9fc385307a8f29f64568be5c039672b4111fb03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 30 Jul 2026 13:38:36 -0300 Subject: [PATCH] Fix percentage-sized inline SVG layout and rendering Resolve percentage dimensions for HTML-backed widgets after their containing block has a definite size. Apply the resolution during rich-text, flex, and grid layout without triggering broad layout invalidation. Rasterize SVGs using their resolved dimensions when intrinsic width or height attributes are missing or percentage-based. Add regression coverage for percentage-sized SVGs and the SS64 navigation buttons and search icon. --- bin/unit_tests/assets/html/ss64.html | 617 ++++++++++++++++++++ include/eepp/ui/uihtmlwidget.hpp | 5 + include/eepp/ui/uisvg.hpp | 2 +- src/eepp/ui/flexlayouter.cpp | 1 + src/eepp/ui/gridlayouter.cpp | 1 + src/eepp/ui/uihtmlwidget.cpp | 105 +++- src/eepp/ui/uirichtext.cpp | 7 +- src/eepp/ui/uisvg.cpp | 28 +- src/tests/unit_tests/htmlsvg_tests.cpp | 34 ++ src/tests/unit_tests/uihtml_float_tests.cpp | 27 + 10 files changed, 804 insertions(+), 23 deletions(-) create mode 100644 bin/unit_tests/assets/html/ss64.html diff --git a/bin/unit_tests/assets/html/ss64.html b/bin/unit_tests/assets/html/ss64.html new file mode 100644 index 000000000..8810f336d --- /dev/null +++ b/bin/unit_tests/assets/html/ss64.html @@ -0,0 +1,617 @@ + + + + + + + + +SS64 Command line reference + + + + + + + + + + +
+ +
+

Command line reference.

+ +
+ +

+ +
+ +

+ + + diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index a4ccb8a81..8282aee0e 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -49,6 +49,11 @@ class EE_API UIHTMLWidget : public UILayout { public: static UIHTMLWidget* New(); + /** Re-resolves percentage width/height for an HTML element once its containing block has a + * used size. This also supports HTML-backed widgets such as UISvg that do not derive from + * UIHTMLWidget. Returns true when the used size or size policy changed. */ + static bool resolvePercentageSize( UIWidget* widget ); + UIHTMLWidget( const std::string& tag = "htmlwidget" ); virtual ~UIHTMLWidget(); diff --git a/include/eepp/ui/uisvg.hpp b/include/eepp/ui/uisvg.hpp index 556b9e2c9..707d66eb9 100644 --- a/include/eepp/ui/uisvg.hpp +++ b/include/eepp/ui/uisvg.hpp @@ -29,7 +29,7 @@ class EE_API UISvg : public UIImage { void loadSvgXml( const pugi::xml_node& node ); void scheduleRasterize(); - void rasterizeSvg( const std::string& svgXml ); + void rasterizeSvg( const std::string& svgXml, const Sizef& targetSize ); void clearThreadTag(); }; diff --git a/src/eepp/ui/flexlayouter.cpp b/src/eepp/ui/flexlayouter.cpp index 2e251971e..c58dfe595 100644 --- a/src/eepp/ui/flexlayouter.cpp +++ b/src/eepp/ui/flexlayouter.cpp @@ -63,6 +63,7 @@ void FlexLayouter::collectFlexItems( SmallVector& items ) { FlexItem item; item.widget = widget; + UIHTMLWidget::resolvePercentageSize( widget ); item.collapsed = isCollapsed; readItemStyle( widget, item ); diff --git a/src/eepp/ui/gridlayouter.cpp b/src/eepp/ui/gridlayouter.cpp index 1aa134f4f..aa064b864 100644 --- a/src/eepp/ui/gridlayouter.cpp +++ b/src/eepp/ui/gridlayouter.cpp @@ -1132,6 +1132,7 @@ void GridLayouter::collectGridItems() { GridItem item; item.widget = widget; + UIHTMLWidget::resolvePercentageSize( widget ); if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) { UIHTMLWidget* htmlChild = widget->asType(); diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index 216e472ab..58e80e55c 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -41,6 +41,23 @@ static bool isAtomicInlineAutoDisplay( CSSDisplay display ) { display == CSSDisplay::InlineGrid; } +static UIWidget* getHTMLContainingBlockParent( const UIWidget* widget ) { + Node* parent = widget->getParent(); + while ( parent && parent->isWidget() && parent->isType( UI_TYPE_HTML_WIDGET ) && + static_cast( parent )->isInline() ) + parent = parent->getParent(); + return parent && parent->isWidget() ? parent->asType() : nullptr; +} + +static bool hasDefiniteCSSHeight( UIWidget* widget ) { + if ( !widget || widget->getLayoutHeightPolicy() != SizePolicy::Fixed ) + return false; + + auto* style = widget->getUIStyle(); + const auto* height = style ? style->getProperty( PropertyId::Height ) : nullptr; + return !( height && StyleSheetLength::isPercentage( height->value() ) ); +} + static CSSBaselineAlignValue parseBaselineAlign( UIHTMLWidget* widget, const StyleSheetProperty& property ) { std::string_view val = property.value(); @@ -76,6 +93,72 @@ UIHTMLWidget* UIHTMLWidget::New() { return eeNew( UIHTMLWidget, () ); } +bool UIHTMLWidget::resolvePercentageSize( UIWidget* widget ) { + if ( widget == nullptr || !( widget->getFlags() & UI_HTML_ELEMENT ) || + widget->getUIStyle() == nullptr ) + return false; + + const auto* width = widget->getUIStyle()->getProperty( PropertyId::Width ); + const auto* height = widget->getUIStyle()->getProperty( PropertyId::Height ); + const bool percentageWidth = width && StyleSheetLength::isPercentage( width->value() ); + const bool percentageHeight = height && StyleSheetLength::isPercentage( height->value() ); + if ( !percentageWidth && !percentageHeight ) + return false; + + UIWidget* containingBlock = getHTMLContainingBlockParent( widget ); + if ( containingBlock == nullptr ) + return false; + + const Rectf contentOffset = containingBlock->getPixelsContentOffset(); + const Sizef containingSize = containingBlock->getPixelsSize(); + const Float contentWidth = + eemax( 0.f, containingSize.getWidth() - contentOffset.Left - contentOffset.Right ); + const Float contentHeight = + eemax( 0.f, containingSize.getHeight() - contentOffset.Top - contentOffset.Bottom ); + Sizef size = widget->getPixelsSize(); + bool changed = false; + + if ( percentageWidth ) { + if ( widget->getLayoutWidthPolicy() != SizePolicy::Fixed ) { + widget->setLayoutWidthPolicy( SizePolicy::Fixed ); + changed = true; + } + Float resolved = widget->cssResolvedLengthToBorderBoxWidth( + widget->convertLength( width->asStyleSheetLength(), contentWidth ) ); + if ( size.getWidth() != resolved ) { + size.setWidth( resolved ); + changed = true; + } + } + + if ( percentageHeight ) { + if ( hasDefiniteCSSHeight( containingBlock ) ) { + if ( widget->getLayoutHeightPolicy() != SizePolicy::Fixed ) { + widget->setLayoutHeightPolicy( SizePolicy::Fixed ); + changed = true; + } + Float resolved = widget->cssResolvedLengthToBorderBoxHeight( + widget->convertLength( height->asStyleSheetLength(), contentHeight ) ); + if ( size.getHeight() != resolved ) { + size.setHeight( resolved ); + changed = true; + } + } else if ( widget->getLayoutHeightPolicy() != SizePolicy::WrapContent ) { + widget->setLayoutHeightPolicy( SizePolicy::WrapContent ); + changed = true; + } + } + + if ( size != widget->getPixelsSize() ) { + if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) + widget->asType()->setInternalPixelsSize( size ); + else + widget->setPixelsSize( size ); + } + + return changed; +} + UIHTMLWidget::UIHTMLWidget( const std::string& tag ) : UILayout( tag ) { mFlags |= UI_HTML_ELEMENT; } @@ -167,34 +250,24 @@ Float UIHTMLWidget::getBaseline() const { } Float UIHTMLWidget::getContainingBlockContentWidth() const { - Node* parent = getParent(); - while ( parent && parent->isWidget() && parent->isType( UI_TYPE_HTML_WIDGET ) && - static_cast( parent )->isInline() ) - parent = parent->getParent(); + UIWidget* parent = getHTMLContainingBlockParent( this ); if ( !parent ) return 0.f; Float width = parent->getPixelsSize().getWidth(); - if ( parent->isWidget() ) { - Rectf contentOffset = parent->asType()->getPixelsContentOffset(); - width -= contentOffset.Left + contentOffset.Right; - } + Rectf contentOffset = parent->getPixelsContentOffset(); + width -= contentOffset.Left + contentOffset.Right; return eemax( 0.f, width ); } Float UIHTMLWidget::getContainingBlockContentHeight() const { - Node* parent = getParent(); - while ( parent && parent->isWidget() && parent->isType( UI_TYPE_HTML_WIDGET ) && - static_cast( parent )->isInline() ) - parent = parent->getParent(); + UIWidget* parent = getHTMLContainingBlockParent( this ); if ( !parent ) return 0.f; Float height = parent->getPixelsSize().getHeight(); - if ( parent->isWidget() ) { - Rectf contentOffset = parent->asType()->getPixelsContentOffset(); - height -= contentOffset.Top + contentOffset.Bottom; - } + Rectf contentOffset = parent->getPixelsContentOffset(); + height -= contentOffset.Top + contentOffset.Bottom; return eemax( 0.f, height ); } diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 535451243..21114c592 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1919,6 +1919,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri return; UIWidget* widget = node->asType(); + UIHTMLWidget::resolvePercentageSize( widget ); // Skip - it must not participate in layout if ( widget->isType( UI_TYPE_HTML_HEAD ) ) @@ -2069,7 +2070,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri widget->getUIStyle() ) { const StyleSheetProperty* wprop = widget->getUIStyle()->getProperty( PropertyId::Width ); - if ( wprop && StyleSheetLength::isPercentage( wprop->value() ) ) { + if ( wprop && StyleSheetLength::isPercentage( wprop->value() ) && + !( widget->getFlags() & UI_HTML_ELEMENT ) ) { Float width = widget->cssWidthPropertyToBorderBoxWidth( *wprop ); widget->setPixelsSize( { width, widget->getPixelsSize().getHeight() } ); } @@ -2078,7 +2080,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri widget->getUIStyle() ) { const StyleSheetProperty* hprop = widget->getUIStyle()->getProperty( PropertyId::Height ); - if ( hprop && StyleSheetLength::isPercentage( hprop->value() ) ) { + if ( hprop && StyleSheetLength::isPercentage( hprop->value() ) && + !( widget->getFlags() & UI_HTML_ELEMENT ) ) { Float height = widget->cssHeightPropertyToBorderBoxHeight( *hprop ); widget->setPixelsSize( { widget->getPixelsSize().getWidth(), height } ); } diff --git a/src/eepp/ui/uisvg.cpp b/src/eepp/ui/uisvg.cpp index 64f6eef2b..201b34a5f 100644 --- a/src/eepp/ui/uisvg.cpp +++ b/src/eepp/ui/uisvg.cpp @@ -68,7 +68,7 @@ void UISvg::scheduleRasterize() { return; if ( !getUISceneNode()->hasThreadPool() ) { - rasterizeSvg( mSvgXml ); + rasterizeSvg( mSvgXml, size ); return; } @@ -76,12 +76,32 @@ void UISvg::scheduleRasterize() { std::string svgXml( mSvgXml ); mTaskId = getUISceneNode()->getThreadPool()->run( - [this, svgXml = std::move( svgXml )] { rasterizeSvg( svgXml ); }, {}, (Uint64)this ); + [this, svgXml = std::move( svgXml ), size] { rasterizeSvg( svgXml, size ); }, {}, + (Uint64)this ); } -void UISvg::rasterizeSvg( const std::string& svgXml ) { +void UISvg::rasterizeSvg( const std::string& svgXml, const Sizef& targetSize ) { + pugi::xml_document document; + std::string rasterXml; + if ( document.load_buffer( svgXml.data(), svgXml.size() ) ) { + pugi::xml_node root = document.document_element(); + auto setRasterDimension = [&]( const char* name, Float value ) { + pugi::xml_attribute attribute = root.attribute( name ); + if ( !attribute ) + attribute = root.append_attribute( name ); + if ( attribute.as_string()[0] == '\0' || + std::string_view( attribute.as_string() ).find( '%' ) != std::string_view::npos ) + attribute.set_value( value ); + }; + setRasterDimension( "width", targetSize.getWidth() ); + setRasterDimension( "height", targetSize.getHeight() ); + XmlStringWriter writer; + document.print( writer ); + rasterXml = std::move( writer.result ); + } + const std::string& source = rasterXml.empty() ? svgXml : rasterXml; TexturePtr texture = TextureFactory::instance()->loadFromMemory( - (const unsigned char*)svgXml.data(), svgXml.size() ); + (const unsigned char*)source.data(), source.size() ); if ( !texture ) return; diff --git a/src/tests/unit_tests/htmlsvg_tests.cpp b/src/tests/unit_tests/htmlsvg_tests.cpp index 99d244d12..a334db7bd 100644 --- a/src/tests/unit_tests/htmlsvg_tests.cpp +++ b/src/tests/unit_tests/htmlsvg_tests.cpp @@ -238,6 +238,40 @@ UTEST( UISvg, svgInsideBlockElement ) { destroyScene( sceneNode ); } +UTEST( UISvg, percentageHeightResolvesAfterContainingBlockLayout ) { + auto sceneNode = createScene(); + ASSERT_TRUE( sceneNode != nullptr ); + + std::string html = R"html( + + + + +)html"; + + auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + ASSERT_TRUE( rootWidget != nullptr ); + sceneNode->updateDirtyLayouts(); + + auto* button = rootWidget->querySelector( "#button" ); + auto* svgWidget = rootWidget->querySelector( "#icon" )->asType(); + ASSERT_TRUE( button != nullptr ); + ASSERT_TRUE( svgWidget != nullptr ); + EXPECT_EQ( svgWidget->getLayoutHeightPolicy(), SizePolicy::Fixed ); + EXPECT_GT( svgWidget->getPixelsSize().getHeight(), 0.f ); + EXPECT_NEAR( svgWidget->getPixelsSize().getHeight(), 34.5f, 1.f ); + + button->setStyleSheetProperty( StyleSheetProperty( "height", "50px" ) ); + sceneNode->updateDirtyLayouts(); + EXPECT_NEAR( svgWidget->getPixelsSize().getHeight(), 50.f, 1.f ); + + destroyScene( sceneNode ); +} + UTEST( UISvg, svgWithMemoryAsset ) { auto sceneNode = createScene(); ASSERT_TRUE( sceneNode != nullptr ); diff --git a/src/tests/unit_tests/uihtml_float_tests.cpp b/src/tests/unit_tests/uihtml_float_tests.cpp index 783a322f1..43163fc62 100644 --- a/src/tests/unit_tests/uihtml_float_tests.cpp +++ b/src/tests/unit_tests/uihtml_float_tests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -614,6 +615,32 @@ UTEST( UIHTMLFloat, floatedListItemsShrinkToFitBlockAnchors ) { Engine::destroySingleton(); } +UTEST( UIHTMLFloat, ss64BlockAnchorsFillButtons ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + std::string html; + ASSERT_TRUE( FileSystem::fileGet( "assets/html/ss64.html", html ) ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + sceneNode->updateDirtyLayouts(); + + auto buttons = sceneNode->getRoot()->querySelectorAll( ".tbtn" ); + auto anchors = sceneNode->getRoot()->querySelectorAll( ".tbtn a" ); + ASSERT_EQ( buttons.size(), (size_t)8 ); + ASSERT_EQ( anchors.size(), buttons.size() ); + for ( size_t i = 0; i < buttons.size(); ++i ) { + auto* anchor = anchors[i]->asType(); + EXPECT_NEAR( anchor->getLineHeightPx(), 40.f, 1.f ); + EXPECT_NEAR( anchor->getPixelsSize().getHeight(), 40.f, 1.f ); + EXPECT_NEAR( anchor->getPixelsSize().getHeight() + 2.f, + buttons[i]->getPixelsSize().getHeight(), 1.f ); + } + auto* svg = sceneNode->getRoot()->querySelector( "#sherlock svg" )->asType(); + ASSERT_TRUE( svg != nullptr ); + EXPECT_EQ( svg->getLayoutHeightPolicy(), SizePolicy::Fixed ); + EXPECT_GT( svg->getPixelsSize().getHeight(), 0.f ); + Engine::destroySingleton(); +} + UTEST( UIHTMLFloat, autoHorizontalMarginsCenterBlockInsideFloat ) { init_float_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();