diff --git a/.agent/rules/build-project.md b/.agent/rules/build-project.md index 816f980f3..bd72d3517 100644 --- a/.agent/rules/build-project.md +++ b/.agent/rules/build-project.md @@ -36,3 +36,17 @@ Run the following command, replacing `` with the correct environment: * Linux: `make -C make/linux -j$(nproc)` * macOS: `make -C make/macosx -j$(sysctl -n hw.ncpu)` * Windows: `make -C make/windows -j%NUMBER_OF_PROCESSORS%` + +## Running GUI Examples Under Xvfb + +Xvfb does not support the multisampled OpenGL contexts requested by some eepp examples. In +particular, `src/examples/ui_html/ui_html.cpp` normally requests 4x MSAA. Launching that binary +through `xvfb-run` or `projects/scripts/xvfb-run-eepp` can therefore fail immediately with +`Could not create window, exiting`, even though the application works on a real display. + +* Do not treat this window-creation failure as evidence of a bug in the feature being tested. +* Unit tests normally request a non-multisampled context and are unaffected. +* For a temporary headless diagnostic of an example, make the diagnostic-only execution path + request 0 MSAA, run it through `projects/scripts/xvfb-run-eepp`, and revert the temporary + example instrumentation afterward. +* Never weaken the example's normal graphics configuration merely to accommodate Xvfb. diff --git a/bin/unit_tests/assets/html/ddg_html.html b/bin/unit_tests/assets/html/ddg_html.html new file mode 100644 index 000000000..07ba59169 --- /dev/null +++ b/bin/unit_tests/assets/html/ddg_html.html @@ -0,0 +1,1417 @@ + + + + + + + + DuckDuckGo HTML: Private Search Without JavaScript + + + + + + + + + + +
+
+
+ +
+ +
+ +
+
+
+ + diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 4d16b809a..b93e24206 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -247,7 +247,6 @@ class EE_API UIHTMLBody : public UIRichText { bool applyProperty( const StyleSheetProperty& attribute ); virtual void updateLayout(); void setDocumentViewportMinHeight( const Float& height ); - void setDocumentCanvasMinHeight( const Float& height ); protected: bool mPropagatedBackground{ false }; diff --git a/premake5.lua b/premake5.lua index 7eb71be33..af8262675 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1,6 +1,23 @@ require "premake.export-compile-commands.export-compile-commands" require "premake.premake-cmake.cmake" -require "premake.premake-ninja.ninja" + +local function premake_is_previous_to_beta8() + if _PREMAKE_VERSION:match("^5%.0%.0[%.%-]alpha%d+$") then + return true + end + + local beta = _PREMAKE_VERSION:match("^5%.0%.0[%.%-]beta(%d+)$") + if beta then + return tonumber(beta) < 8 + end + + return false +end + +-- We will disable this later, since config names are different (local ninja use old naming convention with config=debug or config=release while the new one uses the premake5 convention config=debug_x86_64 and config=release_x86_64) +if true or premake_is_previous_to_beta8() then + require "premake.premake-ninja.ninja" +end newoption { trigger = "with-openssl", description = "Enables OpenSSL support ( and disables mbedtls backend )." } newoption { trigger = "with-dynamic-freetype", description = "Dynamic link against freetype." } diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index fce2af316..79f30638e 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -1168,7 +1169,31 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() { for ( size_t layerIdx = 0; layerIdx < layers.size(); ++layerIdx ) { std::string layerVal = String::trim( layers[layerIdx] ); - std::vector tokens = String::split( layerVal, " ", "", "(" ); + // Whitespace around the background position/size slash is optional. Tokenize it + // without splitting slashes inside functional notation such as url() or var(). + std::vector tokens; + std::string token; + int parenthesisDepth = 0; + for ( const char ch : layerVal ) { + if ( ch == '(' ) + ++parenthesisDepth; + else if ( ch == ')' && parenthesisDepth > 0 ) + --parenthesisDepth; + + if ( parenthesisDepth == 0 && + ( std::isspace( static_cast( ch ) ) || ch == '/' ) ) { + if ( !token.empty() ) { + tokens.emplace_back( std::move( token ) ); + token.clear(); + } + if ( ch == '/' ) + tokens.emplace_back( "/" ); + } else { + token += ch; + } + } + if ( !token.empty() ) + tokens.emplace_back( std::move( token ) ); std::string positionStr; std::string sizeStr; bool hasSlash{ false }; @@ -1196,6 +1221,11 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() { secondBox = tok; } else if ( tok == "/" ) { hasSlash = true; + } else if ( hasSlash && String::startsWith( tok, "var(" ) && !sizeStr.empty() ) { + // eepp resolves custom properties after expanding shorthands. Saved browser + // pages commonly put an image var after an explicit size, for example + // `center / 100% var(--image)`. + imageValues.push_back( tok ); } else if ( hasSlash && !tok.empty() && tok != "/" ) { sizeStr += tok + " "; } else if ( isPositionKeyword( tok ) || String::isNumber( tok[0] ) || diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index a80deb935..839f9a1e3 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -1127,12 +1128,32 @@ void UIHTMLWidget::updateOutOfFlowPosition() { bool useBottom = mBottomEq != "auto"; bool useLeft = mLeftEq != "auto"; bool useRight = mRightEq != "auto"; + auto resetAutoMarginUnlessBothInsetsApply = [this]( Float& marginValue, PropertyId property, + bool bothInsetsApply ) { + if ( bothInsetsApply || !getUIStyle() ) + return; + const auto* marginProperty = getUIStyle()->getProperty( property ); + if ( marginProperty && marginProperty->value() == "auto" ) + marginValue = 0; + }; + // CSS Positioned Layout: an auto margin on an axis only absorbs free space when both + // opposing insets participate in that axis's constraint equation. Otherwise it is zero. + resetAutoMarginUnlessBothInsetsApply( margin.Top, PropertyId::MarginTop, useTop && useBottom ); + resetAutoMarginUnlessBothInsetsApply( margin.Bottom, PropertyId::MarginBottom, + useTop && useBottom ); + resetAutoMarginUnlessBothInsetsApply( margin.Left, PropertyId::MarginLeft, + useLeft && useRight ); + resetAutoMarginUnlessBothInsetsApply( margin.Right, PropertyId::MarginRight, + useLeft && useRight ); // Per CSS §10.1: for absolutely positioned elements, percentage top/bottom // resolves against the containing block's height. If the containing block // does not have a definite height, the percentage computes to auto to // prevent circular dependencies. auto cbHasDefiniteHeight = [&]() { + if ( cb->isType( UI_TYPE_HTML_HTML ) && cb->getUISceneNode() && + cb->getUISceneNode()->getLayoutViewportPixelsSize().getHeight() > 0 ) + return true; if ( !cb->isLayout() ) return true; auto* cbLayout = cb->asType(); diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index a8b2e3320..535451243 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -171,6 +171,18 @@ bool UIHTMLHtml::applyProperty( const StyleSheetProperty& attribute ) { case PropertyId::Width: case PropertyId::Height: return false; // Ignore width and height set from CSS + case PropertyId::MinHeight: { + Float containingBlockHeight = + getUISceneNode() ? getUISceneNode()->getLayoutViewportPixelsSize().getHeight() : 0; + if ( containingBlockHeight <= 0 && getParent() ) + containingBlockHeight = + getParent()->isUINode() + ? getParent()->asType()->getPixelsSize().getHeight() + : getParent()->getSize().getHeight(); + setMinHeight( + convertLengthAsDp( attribute.asStyleSheetLength(), containingBlockHeight ) ); + return true; + } default: break; } @@ -274,23 +286,27 @@ void UIHTMLBody::updateLayout() { } void UIHTMLBody::setDocumentViewportMinHeight( const Float& height ) { - if ( mDocumentViewportMinHeight == height ) - return; mDocumentViewportMinHeight = height; + // The local min-height can be percentage-based and therefore change when the html + // containing block is resized even if the viewport minimum itself did not change. updateDocumentMinHeight(); } -void UIHTMLBody::setDocumentCanvasMinHeight( const Float& height ) { - setDocumentContentMinHeight( height ); -} - Float UIHTMLBody::getLocalMinHeight() const { if ( !getParent() ) return 0; - Float parentHeight = getParent()->isUINode() - ? getParent()->asType()->getPixelsSize().getHeight() - : getParent()->getSize().getHeight(); + Float parentHeight; + if ( getParent()->isType( UI_TYPE_HTML_HTML ) && getUISceneNode() && + getUISceneNode()->getLayoutViewportPixelsSize().getHeight() > 0 ) { + // The root body's containing block is the initial containing block (the viewport), + // not an html box that may already include scrollable overflow. + parentHeight = getUISceneNode()->getLayoutViewportPixelsSize().getHeight(); + } else { + parentHeight = getParent()->isUINode() + ? getParent()->asType()->getPixelsSize().getHeight() + : getParent()->getSize().getHeight(); + } return convertLengthAsDp( mMinHeightLocal, parentHeight ); } diff --git a/src/eepp/ui/uiwebview.cpp b/src/eepp/ui/uiwebview.cpp index 248b4ae40..c61b7eaac 100644 --- a/src/eepp/ui/uiwebview.cpp +++ b/src/eepp/ui/uiwebview.cpp @@ -78,47 +78,13 @@ static void expandWidgetContentExtent( UIWidget* widget, const Vector2f& offset, } static bool syncWidgetDocumentPixelsSize( UIWidget* widget, const Sizef& size ) { - if ( !widget || widget->isClosing() ) + if ( !widget || widget->isClosing() || size == widget->getPixelsSize() ) return false; - Sizef next( size.getWidth(), size.getHeight() ); - if ( next == widget->getPixelsSize() ) - return false; - - widget->setPixelsSize( next ); + widget->setPixelsSize( size ); return true; } -static Float getDocumentBodyPixelsHeight( UIWidget* body, const Sizef& extent ) { - return extent.getHeight(); -} - -static void syncDocumentBoxesPixelsSize( UIWidget* container, const Sizef& extent ) { - if ( !container || extent == Sizef::Zero ) - return; - - UIWidget* html = nullptr; - UIWidget* body = nullptr; - if ( auto htmlNode = container->findByType( UI_TYPE_HTML_HTML ) ) { - if ( htmlNode->isWidget() ) - html = htmlNode->asType(); - } - if ( auto bodyNode = container->findByType( UI_TYPE_HTML_BODY ) ) { - if ( bodyNode->isWidget() ) - body = bodyNode->asType(); - } - - syncWidgetDocumentPixelsSize( container, extent ); - syncWidgetDocumentPixelsSize( html, extent ); - if ( body ) { - if ( body->isType( UI_TYPE_HTML_BODY ) ) - body->asType()->setDocumentCanvasMinHeight( - PixelDensity::pxToDp( extent.getHeight() ) ); - syncWidgetDocumentPixelsSize( - body, { extent.getWidth(), getDocumentBodyPixelsHeight( body, extent ) } ); - } -} - static Sizef computeDocumentContentExtent( UIWidget* container, const Sizef& viewport ) { Sizef extent( viewport.getWidth(), viewport.getHeight() ); if ( !container ) @@ -250,9 +216,9 @@ void UIWebView::updateHTMLMinHeight( UIHTMLHtml* html, UIHTMLBody* body ) { const Rectf bodyMargin = body->getLayoutPixelsMargin(); const Float bodyMarginHeight = PixelDensity::pxToDp( bodyMargin.Top + bodyMargin.Bottom ); html->setMinHeight( h ); - html->setPixelsSize( viewport ); body->setPixelsSize( { viewport.getWidth(), body->getPixelsSize().getHeight() } ); body->setDocumentViewportMinHeight( eemax( 0.f, h - bodyMarginHeight ) ); + html->setPixelsSize( viewport ); } void UIWebView::onSceneChange() { @@ -608,7 +574,7 @@ void UIWebView::updateDocumentMetricsIfNeeded() { containerUpdate(); updateScroll(); } - syncDocumentBoxesPixelsSize( mDocContainer, extent ); + syncWidgetDocumentPixelsSize( mDocContainer, extent ); return extentChanged; }; diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index f47def9ac..46eb4c2fc 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -2659,6 +2659,62 @@ UTEST( UIHTML, BodyViewportMinimumHeightUsesSceneViewport ) { Engine::destroySingleton(); } +UTEST( UIHTML, DuckDuckGoHomepageAbsoluteContentDoesNotGrowBody ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1280, 720, "DuckDuckGo homepage body height", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = init_test_inline_block(); + + auto* vbox = sceneNode->loadLayoutFromString( R"xml( + + + + + + + + + + )xml" ); + ASSERT_TRUE( vbox != nullptr ); + auto* webView = vbox->find( "webview" )->asType(); + ASSERT_TRUE( webView != nullptr ); + + auto* documentScene = webView->getDocumentSceneNode(); + bool navigationCompleted = false; + webView->onNavigationCompleted( + [&navigationCompleted]( const URI& ) { navigationCompleted = true; } ); + webView->loadURI( URI( "assets/html/ddg_html.html" ) ); + + for ( int i = 0; i < 300; ++i ) { + win->getInput()->update(); + SceneManager::instance()->update( Seconds( 1.f / 60.f ) ); + Sys::sleep( Milliseconds( 1 ) ); + } + ASSERT_TRUE( navigationCompleted ); + + auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType(); + auto* content = + documentScene->getRoot()->find( "content_wrapper_homepage" )->asType(); + ASSERT_TRUE( body != nullptr ); + ASSERT_TRUE( content != nullptr ); + EXPECT_NEAR( body->getPixelsSize().getHeight(), + documentScene->getViewportPixelsSize().getHeight(), 2.f ); + EXPECT_NEAR( webView->getDocumentContainer()->getPixelsSize().getHeight(), + documentScene->getViewportPixelsSize().getHeight(), 2.f ); + EXPECT_NEAR( content->getPixelsPosition().y, + documentScene->getLayoutViewportPixelsSize().getHeight() * 0.24f, 2.f ); + EXPECT_LT( content->getPixelsPosition().y, body->getPixelsSize().getHeight() ); + EXPECT_LT( content->getPixelsPosition().y + content->getPixelsSize().getHeight(), + body->getPixelsSize().getHeight() ); + EXPECT_FALSE( webView->getHorizontalScrollBar()->isVisible() ); + EXPECT_FALSE( webView->getVerticalScrollBar()->isVisible() ); + + Engine::destroySingleton(); +} + UTEST( UIHTML, InlineBlock ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Inline Block Test", WindowStyle::Default, WindowBackend::Default, @@ -3668,6 +3724,28 @@ UTEST( UIHTML, ContactFormLayout ) { Engine::destroySingleton(); } +UTEST( UIBackground, shorthandImageVariableAfterSize ) { + auto* specification = StyleSheetSpecification::instance(); + const auto* shorthand = specification->getShorthand( "background" ); + ASSERT_TRUE( shorthand != nullptr ); + + auto properties = shorthand->parse( + "no-repeat center/100% var(--sf-img-1), linear-gradient(transparent, transparent)" ); + auto valueOf = [&properties]( const std::string& name ) -> std::string { + for ( const auto& property : properties ) { + if ( property.getName() == name ) + return property.getValue(); + } + return {}; + }; + + EXPECT_STDSTREQ( valueOf( "background-image" ), + "var(--sf-img-1),linear-gradient(transparent, transparent)" ); + EXPECT_STDSTREQ( valueOf( "background-size" ), "100%,auto" ); + EXPECT_STDSTREQ( valueOf( "background-position-x" ), "center,0%" ); + EXPECT_STDSTREQ( valueOf( "background-position-y" ), "center,0%" ); +} + UTEST( UIBackground, imageAtlasPositioning ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 653, "Background Atlas Test", VisualTestWindowStyle,