From 68904b96564ba9520aa16430bd9b731e4d568d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 6 Jun 2026 15:54:54 -0300 Subject: [PATCH] Fixed layouting performance issues in UIRichText (layout invalidation trashing issues). Everything is highly commented given that none of these changes are obvious and it's very easy to break it. Added tests to verify possible regression. And added a simple benchmark with some metrics to inspect layouting invalidation issues. --- include/eepp/ui/uilayout.hpp | 22 ++++ include/eepp/ui/uiscenenode.hpp | 3 +- src/benchmarks/inline_layout_benchmark.cpp | 121 ++++++++++++++++++ src/eepp/ui/uilayout.cpp | 45 ++++++- src/eepp/ui/uirichtext.cpp | 85 ++++++++---- src/eepp/ui/uiscenenode.cpp | 25 +++- .../ui_markdownview/ui_markdownview.cpp | 71 ++++++++-- src/tests/unit_tests/uihtml_tests.cpp | 67 ++++++++++ 8 files changed, 401 insertions(+), 38 deletions(-) diff --git a/include/eepp/ui/uilayout.hpp b/include/eepp/ui/uilayout.hpp index 4beea375b..186f88781 100644 --- a/include/eepp/ui/uilayout.hpp +++ b/include/eepp/ui/uilayout.hpp @@ -9,6 +9,15 @@ class GridLayouter; class EE_API UILayout : public UIWidget { public: + struct Metrics { + Uint64 autoSizeChildren{ 0 }; + Uint64 childCountChanges{ 0 }; + Uint64 invalidations{ 0 }; + Uint64 richTextRebuilds{ 0 }; + Uint64 synchronousUpdates{ 0 }; + Uint64 treeUpdates{ 0 }; + }; + virtual Uint32 getType() const; virtual bool isType( const Uint32& type ) const; @@ -29,6 +38,14 @@ class EE_API UILayout : public UIWidget { void setLayoutDirty(); + static void resetMetrics(); + + static Metrics getMetrics(); + + static void setMetricsEnabled( bool enabled ); + + static void countRichTextRebuild(); + protected: friend class GridLayouter; friend class UISceneNode; @@ -36,6 +53,11 @@ class EE_API UILayout : public UIWidget { UnorderedSet mLayouts; bool mDirtyLayout{ false }; + // True only while updateLayoutTree() is actively updating this layout and walking its + // descendants. This is intentionally narrower than mDirtyLayout: code that receives layout + // notifications can use it to distinguish "already scheduled for a future pass" from + // "currently rebuilding/positioning children, so re-entering now would duplicate work." + bool mUpdatingLayoutTree{ false }; bool mPacking{ false }; bool mGravityOwner{ false }; diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 0f130e965..01b3af5a4 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -787,10 +787,11 @@ class EE_API UISceneNode : public SceneNode { UnorderedSet mDirtyStyleState; UnorderedMap mDirtyStyleStateCSSAnimations; UnorderedSet mDirtyLayouts; + SmallVector mDirtyLayoutsSnapshot; std::vector> mTimes; ColorSchemePreference mColorSchemePreference{ ColorSchemePreference::Dark }; ContrastPreference mContrastPreference{ ContrastPreference::NoPreference }; - Uint32 mMaxInvalidationDepth{ 2 }; + Uint32 mMaxInvalidationDepth{ 3 }; Node* mCurParent{ nullptr }; Uint32 mCurOnSizeChangeListener{ 0 }; Uint32 mCurrentMarker{ 0 }; diff --git a/src/benchmarks/inline_layout_benchmark.cpp b/src/benchmarks/inline_layout_benchmark.cpp index 44a994a37..f8fa3a409 100644 --- a/src/benchmarks/inline_layout_benchmark.cpp +++ b/src/benchmarks/inline_layout_benchmark.cpp @@ -3,20 +3,39 @@ #include #include #include +#include #include #include #include +#include +#include +#include +#include #include +#include + using namespace EE; using namespace EE::Graphics; +using namespace EE::Scene; +using namespace EE::UI; using namespace EE::Window; static constexpr int numBoxes = 100; static constexpr int numSpansPerBox = 20; static constexpr int layoutIterations = 50; +static constexpr int markdownFlushIterations = 1; static constexpr Float maxWidth = 800; +static int getMarkdownFlushIterations() { + if ( const char* env = std::getenv( "EE_MARKDOWN_BENCH_FLUSH_ITERATIONS" ) ) { + Int32 val = markdownFlushIterations; + if ( String::fromString( val, std::string( env ) ) ) + return eemax( 0, val ); + } + return markdownFlushIterations; +} + UTEST( Benchmark, InlineLayout ) { Engine::instance()->createWindow( WindowSettings( 800, 600, "bench", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ) ); @@ -81,3 +100,105 @@ UTEST( Benchmark, InlineLayout ) { Engine::destroySingleton(); } + +UTEST( Benchmark, MarkdownReadme ) { + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + const std::string readmePath = "../../README.md"; + std::string markdown; + if ( !FileSystem::fileGet( readmePath, markdown ) ) { + UTEST_PRINT_INFO( + String::format( "Failed to load %s from cwd '%s', skipping benchmark", + readmePath.c_str(), FileSystem::getCurrentWorkingDirectory().c_str() ) + .c_str() ); + return; + } + + EE::Window::Window* window = Engine::instance()->createWindow( + WindowSettings( 1280, 720, "markdown bench", WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ) ); + if ( !window || !window->isOpen() ) { + Engine::destroySingleton(); + UTEST_PRINT_INFO( "Failed to create window, skipping benchmark" ); + return; + } + Engine::instance()->disableSharedGLContext(); + + PixelDensity::setPixelDensity( 1.f ); + UISceneNode* ui = UISceneNode::New( window ); + SceneManager::instance()->add( ui ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + FontTrueType* monoFont = FontTrueType::New( "monospace" ); + monoFont->loadFromFile( "../assets/fonts/DejaVuSansMono.ttf" ); + if ( !font->loaded() || !monoFont->loaded() ) { + Engine::destroySingleton(); + UTEST_PRINT_INFO( "Failed to load fonts, skipping benchmark" ); + return; + } + monoFont->setEnableDynamicMonospace( true ); + FontFamily::loadFromRegular( font ); + FontFamily::loadFromRegular( monoFont ); + ui->getUIThemeManager()->setDefaultFont( font ); + + UIScrollView* scrollView = UIScrollView::New(); + scrollView->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + scrollView->setParent( ui->getRoot() ); + + UIMarkdownView* markdownView = UIMarkdownView::New(); + markdownView->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + markdownView->setPadding( Rectf( 16, 16, 16, 16 ) ); + markdownView->setParent( scrollView->getContainer() ); + + UILayout::resetMetrics(); + + Clock loadClock; + markdownView->loadFromString( markdown ); + Time loadElapsed = loadClock.getElapsedTime(); + UILayout::Metrics afterLoad = UILayout::getMetrics(); + + int flushIterations = getMarkdownFlushIterations(); + Clock updateClock; + for ( int i = 0; i < flushIterations; ++i ) + ui->update( Seconds( 0 ) ); + Time updateElapsed = updateClock.getElapsedTime(); + UILayout::Metrics afterUpdate = UILayout::getMetrics(); + + if ( flushIterations > 0 ) + EXPECT_GT( markdownView->getPixelsSize().getHeight(), 0.f ); + + UTEST_PRINT_INFO( String::format( "README path: %s", readmePath.c_str() ).c_str() ); + UTEST_PRINT_INFO( String::format( "README bytes: %zu", markdown.size() ).c_str() ); + UTEST_PRINT_INFO( String::format( "Flush iterations: %d", flushIterations ).c_str() ); + UTEST_PRINT_INFO( String::format( "Load: %s", loadElapsed.toString().c_str() ).c_str() ); + UTEST_PRINT_INFO( + String::format( "Layout flush: %s", updateElapsed.toString().c_str() ).c_str() ); + UTEST_PRINT_INFO( + String::format( "After load - child changes: %llu, auto-size children: %llu, rich text " + "rebuilds: %llu, invalidations: %llu, sync updates: %llu, tree updates: " + "%llu", + (unsigned long long)afterLoad.childCountChanges, + (unsigned long long)afterLoad.autoSizeChildren, + (unsigned long long)afterLoad.richTextRebuilds, + (unsigned long long)afterLoad.invalidations, + (unsigned long long)afterLoad.synchronousUpdates, + (unsigned long long)afterLoad.treeUpdates ) + .c_str() ); + UTEST_PRINT_INFO( + String::format( "After flush - child changes: %llu, auto-size children: %llu, rich text " + "rebuilds: %llu, invalidations: %llu, sync updates: %llu, tree updates: " + "%llu", + (unsigned long long)afterUpdate.childCountChanges, + (unsigned long long)afterUpdate.autoSizeChildren, + (unsigned long long)afterUpdate.richTextRebuilds, + (unsigned long long)afterUpdate.invalidations, + (unsigned long long)afterUpdate.synchronousUpdates, + (unsigned long long)afterUpdate.treeUpdates ) + .c_str() ); + + UILayout::setMetricsEnabled( false ); + SceneManager::instance()->remove( ui ); + eeDelete( ui ); + Engine::destroySingleton(); +} diff --git a/src/eepp/ui/uilayout.cpp b/src/eepp/ui/uilayout.cpp index 36fb1e2f2..c57562197 100644 --- a/src/eepp/ui/uilayout.cpp +++ b/src/eepp/ui/uilayout.cpp @@ -3,12 +3,18 @@ namespace EE { namespace UI { +static UILayout::Metrics sMetrics; +static bool sMetricsEnabled{ false }; + UILayout::UILayout( const std::string& tag ) : UIWidget( tag ) { mNodeFlags |= NODE_FLAG_LAYOUT; unsetFlags( UI_TAB_FOCUSABLE ); } void UILayout::onChildCountChange( Node* child, const bool& removed ) { + if ( sMetricsEnabled ) + sMetrics.childCountChanges++; + UIWidget::onChildCountChange( child, removed ); if ( child->isLayout() ) { @@ -63,6 +69,9 @@ void UILayout::updateLayout() {} void UILayout::setLayoutDirty() { if ( !mDirtyLayout ) { + if ( sMetricsEnabled ) + sMetrics.invalidations++; + mUISceneNode->invalidateLayout( this ); mDirtyLayout = true; } @@ -78,19 +87,29 @@ void UILayout::setGravityOwner( bool gravityOwner ) { void UILayout::tryUpdateLayout() { if ( mUISceneNode->isUpdatingLayouts() ) { - updateLayout(); + if ( !isPacking() ) { + if ( sMetricsEnabled ) { + sMetrics.synchronousUpdates++; + } + updateLayout(); + } } else if ( !mDirtyLayout ) { setLayoutDirty(); } } void UILayout::updateLayoutTree() { + if ( sMetricsEnabled ) + sMetrics.treeUpdates++; + + mUpdatingLayoutTree = true; updateLayout(); for ( auto layout : mLayouts ) { layout->updateLayoutTree(); } + mUpdatingLayoutTree = false; onLayoutUpdate(); } @@ -138,10 +157,32 @@ void UILayout::updateLayoutWrappingContents() { } void UILayout::onAutoSizeChild( UIWidget* child ) { + if ( sMetricsEnabled ) + sMetrics.autoSizeChildren++; + if ( child->isLayout() ) { child->asType()->updateLayoutWrappingContents(); - } else + } else { child->onAutoSize(); + } +} + +void UILayout::resetMetrics() { + sMetrics = {}; + sMetricsEnabled = true; +} + +UILayout::Metrics UILayout::getMetrics() { + return sMetrics; +} + +void UILayout::setMetricsEnabled( bool enabled ) { + sMetricsEnabled = enabled; +} + +void UILayout::countRichTextRebuild() { + if ( sMetricsEnabled ) + sMetrics.richTextRebuilds++; } }} // namespace EE::UI diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index e0b3b70ff..0c2c09ad4 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1078,18 +1078,22 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) { } void UIRichText::onSizeChange() { - if ( getCSSPosition() == CSSPosition::Fixed ) { - // Fixed-position elements are sized relative to the viewport. Do not - // trigger a self-layout pass (tryUpdateLayout via - // UIHTMLWidget::onSizeChange) nor propagate size changes upward - // (notifyLayoutAttrChangeParent): neither ancestors nor this element - // need to re-layout in response to a viewport-relative size change, - // and doing so would cause unnecessary re-layout or infinite recursion. + if ( isOutOfFlow() ) { + // Out-of-flow elements are sized/positioned by updateOutOfFlowPosition(). That routine can + // apply a computed width/height after the normal layouter has run. Running + // UIHTMLWidget::onSizeChange() from here would call tryUpdateLayout() on the same element + // while updateOutOfFlowPosition() is still on the stack, which can recurse forever through + // updateLayout() -> updateOutOfFlowPosition() -> setPixelsSize() -> onSizeChange(). + // + // We still emit the generic UIWidget size-change notification so drawing/events stay + // current, and absolute elements still notify ancestors below: body-like containers may + // include absolute descendants when computing their effective content height. Fixed + // elements are excluded from the parent notification because they are viewport-relative and + // do not participate in normal-flow sizing. UIWidget::onSizeChange(); } else { UIHTMLWidget::onSizeChange(); } - notifyLayoutAttrChange(); if ( getCSSPosition() != CSSPosition::Fixed ) notifyLayoutAttrChangeParent(); } @@ -1590,6 +1594,7 @@ static Drawable* getInlineBorderDrawable( UIWidget* widget ) { } void UIRichText::rebuildRichText( UILayout* container, RichText& richText, IntrinsicMode mode ) { + UILayout::countRichTextRebuild(); richText.clear(); if ( container->isType( UI_TYPE_RICHTEXT ) || container->isType( UI_TYPE_TEXTSPAN ) ) { auto* uiRt = static_cast( container ); @@ -2170,24 +2175,58 @@ Uint32 UIRichText::onMessage( const NodeMessage* Msg ) { bool packing = isPacking(); if ( packing ) return 1; - if ( Msg->getSender() != this ) { + bool senderIsFixed = Msg->getSender()->isType( UI_TYPE_HTML_WIDGET ) && + static_cast( Msg->getSender() ) + ->getCSSPosition() == CSSPosition::Fixed; + + // updateOutOfFlowPosition() may set this element's own size while the scene is already + // updating layouts. That self size-change message has already been accounted for by + // the out-of-flow positioning routine; responding to it by relaying into tryUpdateLayout() + // re-enters updateOutOfFlowPosition() and can recurse until stack overflow. Parent + // notifications for absolute elements are handled by onSizeChange(), so ignoring this + // self-message does not hide the size change from ancestors that need it. + if ( Msg->getSender() == this && isOutOfFlow() && mUISceneNode->isUpdatingLayouts() ) + return 1; + + // A block RichText owns an inline formatting stream built from its descendants. + // During updateLayoutTree(), BlockLayouter rebuilds that stream, measures embedded + // widgets, and positions children. Those child size/position changes naturally emit + // LayoutAttributeChange messages back to this RichText. Re-entering this same block + // immediately would throw away the stream being built and rebuild it again while the + // first pass is still positioning descendants. Documents expose this as + // thousands of redundant rebuildRichText()/onAutoSizeChild() calls per frame. + // + // When mUpdatingLayoutTree is true, the current pass is already incorporating these + // descendant measurements, so the correct action is to invalidate intrinsic width + // caches and absorb the message. External changes, async image resize notifications, + // and normal post-layout mutations arrive outside this active pass and still follow the + // regular notifyLayoutAttrChangeParent()/tryUpdateLayout() path below. + if ( !senderIsFixed && mUISceneNode->isUpdatingLayouts() && mUpdatingLayoutTree && + !isOutOfFlow() ) { invalidateIntrinsicSize(); - // Fixed-position children are sized relative to the viewport - // and never affect the parent's normal-flow layout. Suppress - // ancestor notification and parent re-layout for these; absolute - // children are allowed because parents like body compute their - // minHeight from absolute children. - if ( !Msg->getSender()->isType( UI_TYPE_HTML_WIDGET ) || - static_cast( Msg->getSender() )->getCSSPosition() != - CSSPosition::Fixed ) { - notifyLayoutAttrChangeParent(); - } + return 1; } - if ( !Msg->getSender()->isType( UI_TYPE_HTML_WIDGET ) || - static_cast( Msg->getSender() )->getCSSPosition() != - CSSPosition::Fixed ) { + + if ( Msg->getSender() != this ) { + // A descendant change invalidates the inline/block formatting stream owned by this + // RichText. Do not immediately relay that invalidation to the parent: the parent's + // layout depends on this RichText's box, not on the raw descendant event. Relaying + // first lets dirty-layout coalescing replace this dirty RichText with an ancestor + // layout, and generic ancestors such as UILinearLayout measure children before the + // recursive child update runs. Delayed image loads then leave the ancestor with the + // old RichText height until an unrelated hover/resize causes another invalidation. + // + // Rebuild this RichText instead. If the rebuilt stream changes the RichText box, + // UIRichText::onSizeChange() will notify ancestors with the actual size change; if + // the box does not change, no ancestor layout work was necessary. Fixed-position + // descendants are still ignored below because they are viewport-relative and do not + // affect this normal-flow box. + invalidateIntrinsicSize(); + } + + if ( !senderIsFixed ) tryUpdateLayout(); - } + return 1; } case NodeMessage::MouseDown: { diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 9032a4fe5..76e87feed 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -916,13 +916,34 @@ void UISceneNode::setIsLoading( bool isLoading ) { void UISceneNode::updateDirtyLayouts() { if ( !mDirtyLayouts.empty() ) { Clock clock; + + // Process a snapshot instead of iterating mDirtyLayouts directly. Layout is allowed to + // invalidate more layouts while this pass is running: size changes can happen as children + // are measured, async resources can resolve, and HTML/RichText can discover that a parent + // needs another pass. Those new invalidations must remain in mDirtyLayouts for the outer + // invalidation-depth loop to process next. If we iterate the live set and clear it at the + // end, any invalidation created during layout is silently lost; if we force everything to + // update synchronously instead, RichText/block layout can re-enter the same parent many + // times and rebuild the same inline stream repeatedly. + // + // Keep the snapshot as reusable storage instead of moving mDirtyLayouts into a temporary + // set. Moving the set transfers its bucket/node allocation and frees it at the end of this + // function, which makes large documents rebuild the dirty-set allocation on every layout + // wave. Copying only layout pointers into a SmallVector keeps common passes inline, lets + // large bursts grow once and retain capacity, and mDirtyLayouts.clear() preserves the set + // buckets for invalidations produced by the current pass. + mDirtyLayoutsSnapshot.clear(); + mDirtyLayoutsSnapshot.reserve( mDirtyLayouts.size() ); + for ( auto layout : mDirtyLayouts ) + mDirtyLayoutsSnapshot.push_back( layout ); + mDirtyLayouts.clear(); + mUpdatingLayouts = true; - for ( UILayout* layout : mDirtyLayouts ) { + for ( UILayout* layout : mDirtyLayoutsSnapshot ) { layout->updateLayoutTree(); } - mDirtyLayouts.clear(); mUpdatingLayouts = false; if ( mVerbose ) diff --git a/src/examples/ui_markdownview/ui_markdownview.cpp b/src/examples/ui_markdownview/ui_markdownview.cpp index c1ee6d024..5c0d65d64 100644 --- a/src/examples/ui_markdownview/ui_markdownview.cpp +++ b/src/examples/ui_markdownview/ui_markdownview.cpp @@ -1,14 +1,59 @@ #include -#include -EE_MAIN_FUNC int main( int, char** ) { +#include +#include + +EE_MAIN_FUNC int main( int argc, char* argv[] ) { UIApplication app( { 1280, 720, "eepp - UIMarkdownView Example" }, {}, ContextSettings( false, ContextSettings::FrameRateLimitScreenRefreshRate, 4 ) ); - app.getUI()->loadLayoutFromString( R"xml( + args::ArgumentParser parser( "eepp Markdown View Example" ); + args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } ); + + args::Positional uri( parser, "URI", "The local URI to load" ); + args::ValueFlag prefersColorScheme( + parser, "prefers-color-scheme", + "Set the preferred color scheme (\"light\", \"dark\" or \"system\")", + { 'c', "prefers-color-scheme" } ); + args::Flag benchmarkMode( parser, "benchmark-mode", + "Render as much as possible to measure the rendering performance.", + { "benchmark-mode" } ); + args::ValueFlag pixelDensityConf( parser, "pixel-density", + "Set default application pixel density", + { 'd', "pixel-density" } ); + + try { + parser.ParseCLI( Sys::parseArguments( argc, argv ) ); + } catch ( const args::Help& ) { + std::cout << parser; + return EXIT_SUCCESS; + } catch ( const args::ParseError& e ) { + std::cerr << e.what() << std::endl; + std::cerr << parser; + return EXIT_FAILURE; + } catch ( args::ValidationError& e ) { + std::cerr << e.what() << std::endl; + std::cerr << parser; + return EXIT_FAILURE; + } + + auto ui = app.getUI(); + ui->setColorSchemePreference( + !prefersColorScheme.Get().empty() + ? ColorSchemePreferences::fromStringExt( prefersColorScheme.Get() ) + : ColorSchemeExtPreference::Dark ); + + ui->loadLayoutFromString( R"xml( - + + + )xml" ); + + auto markdownView = ui->find( "markdown_view" ); + + if ( uri.Get().empty() ) { + markdownView->loadFromString( R"markdown( # Markdown Header 1 ## Markdown Header 2 ### Markdown Header 3 @@ -35,11 +80,17 @@ void main() { printf("Hello World"); } ``` - - - )xml" ); + )markdown" ); + } else if ( FileSystem::fileExists( uri.Get() ) ) { + std::string file( uri.Get() ); + std::string data; + FileSystem::fileGet( file, data ); + markdownView->loadFromString( data ); + } else { + std::cout << "File not found\n"; - auto markdownView = app.getUI()->find( "markdown_view" ); + return EXIT_FAILURE; + } app.getWindow()->getInput()->pushCallback( [markdownView]( InputEvent* event ) { switch ( event->Type ) { @@ -59,9 +110,9 @@ void main() { } } ); - app.getUI()->on( Event::KeyUp, [&app]( const Event* event ) { + ui->on( Event::KeyUp, [ui]( const Event* event ) { if ( event->asKeyEvent()->getKeyCode() == KEY_F11 ) { - UIWidgetInspector::create( app.getUI() ); + UIWidgetInspector::create( ui ); } } ); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index a18c80a95..24d62b5c1 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -4,7 +4,9 @@ #include #include +#include #include +#include #include #include #include @@ -17,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -3982,6 +3985,70 @@ UTEST( UIHTML, ImagePercentageWidthRespectsParentMaxWidth ) { Engine::destroySingleton(); } +UTEST( UIHTML, TextureReplaceInvalidatesRichTextAncestors ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 768, "texture replace img relayout", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_inline_block(); + + UIWebView* webView = UIWebView::New(); + webView->setParent( sceneNode->getRoot() ); + webView->setPixelsSize( win->getWidth(), win->getHeight() ); + webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + std::string html = R"html( + + + +
+

Before image

+ +

After image

+
+ + + )html"; + sceneNode->setURI( "file://delayed-image-resize.html" ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), webView->getDocumentContainer(), + String::hash( "delayed-image-resize" ) ); + + win->getInput()->update(); + SceneManager::instance()->update(); + sceneNode->updateDirtyLayouts(); + + auto* article = sceneNode->getRoot()->find( "article" )->asType(); + auto* body = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType(); + auto* doc = webView->getDocumentContainer(); + auto images = sceneNode->getRoot()->findAllByTag( "img" ); + ASSERT_TRUE( article != nullptr ); + ASSERT_TRUE( body != nullptr ); + ASSERT_TRUE( doc != nullptr ); + ASSERT_EQ( images.size(), (size_t)1 ); + auto* img = images[0]->asType(); + ASSERT_TRUE( img != nullptr ); + + Texture* texture = TextureFactory::instance()->createEmptyTexture( 1, 1, 4, Color::Transparent ); + ASSERT_TRUE( texture != nullptr ); + img->setDrawable( texture ); + sceneNode->updateDirtyLayouts(); + + Float articleInitialHeight = article->getPixelsSize().getHeight(); + Float bodyInitialHeight = body->getPixelsSize().getHeight(); + Float docInitialHeight = doc->getPixelsSize().getHeight(); + + Image loadedImage( 320, 180, 4, Color::White ); + texture->replace( &loadedImage ); + SceneManager::instance()->update(); + sceneNode->updateDirtyLayouts(); + + EXPECT_GT( article->getPixelsSize().getHeight(), articleInitialHeight + 150.f ); + EXPECT_GT( body->getPixelsSize().getHeight(), bodyInitialHeight + 150.f ); + EXPECT_GT( doc->getPixelsSize().getHeight(), docInitialHeight + 150.f ); + + Engine::destroySingleton(); +} + UTEST( UIHTML, ImageCSSWidthOverridesHTMLWidthAttribute ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "img css width overrides html width attr", WindowStyle::Default,