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.
This commit is contained in:
Martín Lucas Golini
2026-07-30 13:38:36 -03:00
parent 4d644880d8
commit 9fc385307a
10 changed files with 804 additions and 23 deletions
+1
View File
@@ -63,6 +63,7 @@ void FlexLayouter::collectFlexItems( SmallVector<FlexItem, 16>& items ) {
FlexItem item;
item.widget = widget;
UIHTMLWidget::resolvePercentageSize( widget );
item.collapsed = isCollapsed;
readItemStyle( widget, item );
+1
View File
@@ -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<UIHTMLWidget>();
+89 -16
View File
@@ -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<UIHTMLWidget*>( parent )->isInline() )
parent = parent->getParent();
return parent && parent->isWidget() ? parent->asType<UIWidget>() : 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<UIHTMLWidget>()->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<UIHTMLWidget*>( 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<UIWidget>()->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<UIHTMLWidget*>( 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<UIWidget>()->getPixelsContentOffset();
height -= contentOffset.Top + contentOffset.Bottom;
}
Rectf contentOffset = parent->getPixelsContentOffset();
height -= contentOffset.Top + contentOffset.Bottom;
return eemax( 0.f, height );
}
+5 -2
View File
@@ -1919,6 +1919,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
return;
UIWidget* widget = node->asType<UIWidget>();
UIHTMLWidget::resolvePercentageSize( widget );
// Skip <head> - 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 } );
}
+24 -4
View File
@@ -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;
+34
View File
@@ -238,6 +238,40 @@ UTEST( UISvg, svgInsideBlockElement ) {
destroyScene( sceneNode );
}
UTEST( UISvg, percentageHeightResolvesAfterContainingBlockLayout ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<button id="button" style="height:34.5px;padding:2px 6px 3px">
<svg id="icon" viewBox="0 0 26 26" style="width:100%;height:100%">
<path d="M11 2a9 9 0 105.641 16.01l3.652 3.697 1.414-1.414-3.697-3.653A9 9 0 0011 2z"/>
</svg>
</button>
</body>
</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<UISvg>();
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 );
@@ -8,6 +8,7 @@
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uisvg.hpp>
#include <eepp/ui/uitextnode.hpp>
#include <eepp/ui/uitheme.hpp>
#include <eepp/ui/uithememanager.hpp>
@@ -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<UIRichText>();
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<UISvg>();
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();