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();