diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp index 1cd510a39..e48430b28 100644 Binary files a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp differ diff --git a/bin/unit_tests/assets/html/image_width.html b/bin/unit_tests/assets/html/image_width.html new file mode 100644 index 000000000..1a0681cd3 --- /dev/null +++ b/bin/unit_tests/assets/html/image_width.html @@ -0,0 +1,28 @@ + + + + +
+
+representative_image +
+
+ diff --git a/bin/unit_tests/assets/html/image_width_2.html b/bin/unit_tests/assets/html/image_width_2.html new file mode 100644 index 000000000..fdec41eaf --- /dev/null +++ b/bin/unit_tests/assets/html/image_width_2.html @@ -0,0 +1,34 @@ + + + + + + +
+ +
+ + diff --git a/bin/unit_tests/assets/html/image_width_3.html b/bin/unit_tests/assets/html/image_width_3.html new file mode 100644 index 000000000..efe93e02c --- /dev/null +++ b/bin/unit_tests/assets/html/image_width_3.html @@ -0,0 +1,209 @@ + + + + + + +
+
+
+

+ Cards on a webpage, scrolled into the viewport, completing their animation (turning to land flat), as they reach 50%. +

+
+
+
+ + diff --git a/bin/unit_tests/assets/html/image_width_3.webp b/bin/unit_tests/assets/html/image_width_3.webp new file mode 100644 index 000000000..37f60622b Binary files /dev/null and b/bin/unit_tests/assets/html/image_width_3.webp differ diff --git a/src/eepp/ui/css/stylesheetselectorrule.cpp b/src/eepp/ui/css/stylesheetselectorrule.cpp index dc1bcc080..51018a3e9 100644 --- a/src/eepp/ui/css/stylesheetselectorrule.cpp +++ b/src/eepp/ui/css/stylesheetselectorrule.cpp @@ -29,9 +29,11 @@ static bool isPseudoClassState( const std::string& pseudoClass ) { } static const char* StructuralPseudoClasses[] = { - "checked", "disabled", "empty", "enabled", "first-child", - "first-of-type", "last-child", "last-of-type", "not", "nth-child", - "nth-last-child", "nth-last-of-type", "nth-of-type", "only-of-type", "only-child" }; + "checked", "disabled", "empty", "enabled", + "first-child", "first-of-type", "last-child", "last-of-type", + "not", "nth-child", "nth-last-child", "nth-last-of-type", + "nth-of-type", "only-of-type", "only-child", "where", + "is" }; static bool isStructuralPseudoClass( const std::string& pseudoClass ) { for ( Uint32 i = 0; i < eeARRAY_SIZE( StructuralPseudoClasses ); i++ ) { @@ -236,7 +238,12 @@ void StyleSheetSelectorRule::parseFragment( const std::string& selectorFragment if ( !mStructuralPseudoClasses.empty() ) { mRequirementFlags |= StructuralPseudoClass; - mSpecificity += SpecificityStructuralPseudoClass * mStructuralPseudoClasses.size(); + size_t count = 0; + for ( const auto& spc : mStructuralPseudoClasses ) { + if ( spc != "where" ) + count++; + } + mSpecificity += SpecificityStructuralPseudoClass * count; } } diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 66876f1a3..01269e10b 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -626,6 +627,11 @@ static bool isNth( int a, int b, int count ) { return ( x >= 0 && x * a + b == count ); } +static bool whereIsMatch( const UIWidget* node, const std::string& param ) { + StyleSheetSelectorRule rule( param, StyleSheetSelectorRule::PatternMatch::ANY ); + return rule.matches( const_cast( node ) ); +} + void StyleSheetSpecification::registerDefaultNodeSelectors() { mNodeSelectors["empty"] = []( const UIWidget* node, int, int, const FunctionString& ) -> bool { return node->getFirstChild() == NULL; @@ -739,6 +745,22 @@ void StyleSheetSpecification::registerDefaultNodeSelectors() { } return false; }; + + mNodeSelectors["where"] = []( const UIWidget* node, int, int, + const FunctionString& data ) -> bool { + if ( data.isEmpty() || data.getParameters().empty() || + ( data.getName() != "where" && data.getName() != "is" ) ) + return false; + + for ( const auto& param : data.getParameters() ) { + if ( !param.empty() && whereIsMatch( node, param ) ) + return true; + } + return false; + }; + + auto whereFn = mNodeSelectors["where"]; + mNodeSelectors["is"] = whereFn; } StructuralSelector StyleSheetSpecification::getStructuralSelector( const std::string& name ) { diff --git a/src/eepp/ui/uiimage.cpp b/src/eepp/ui/uiimage.cpp index 9635463f1..fe0780bb2 100644 --- a/src/eepp/ui/uiimage.cpp +++ b/src/eepp/ui/uiimage.cpp @@ -146,6 +146,17 @@ void UIImage::onAutoSize() { } } else if ( mHeightPolicy == SizePolicy::WrapContent ) { Float contentWidth = std::max( 0.f, size.x - mPaddingPx.Left - mPaddingPx.Right ); + + if ( !mMaxWidthEq.empty() ) { + Float maxWidth = + lengthFromValue( mMaxWidthEq, CSS::PropertyRelativeTarget::ContainingBlockWidth ); + Float maxContentWidth = std::max( 0.f, maxWidth - mPaddingPx.Left - mPaddingPx.Right ); + if ( contentWidth > maxContentWidth ) { + contentWidth = maxContentWidth; + size.x = maxWidth; + } + } + size.y = (int)( contentWidth * ( drawableSize.getHeight() / drawableSize.getWidth() ) ) + mPaddingPx.Top + mPaddingPx.Bottom; if ( !mMaxHeightEq.empty() ) { diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index 5ea4d6c18..382fb2dd0 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -1634,15 +1635,17 @@ Float UINode::getPropertyRelativeTargetContainerLength( Float containerLength = defaultValue; switch ( relativeTarget ) { case PropertyRelativeTarget::ContainingBlockWidth: { - Node* parent = getParent(); // Text spans cannot be considered containing blocks - while ( parent->isType( UI_TYPE_TEXTSPAN ) ) + Node* parent = getParent(); + while ( parent && parent->isWidget() && parent->isType( UI_TYPE_HTML_WIDGET ) && + static_cast( parent )->isInline() ) parent = parent->getParent(); containerLength = parent ? parent->getPixelsSize().getWidth() : 0; break; } case PropertyRelativeTarget::ContainingBlockHeight: { - Node* parent = getParent(); // Text spans cannot be considered containing blocks - while ( parent->isType( UI_TYPE_TEXTSPAN ) ) + Node* parent = getParent(); + while ( parent && parent->isWidget() && parent->isType( UI_TYPE_HTML_WIDGET ) && + static_cast( parent )->isInline() ) parent = parent->getParent(); containerLength = parent ? parent->getPixelsSize().getHeight() : 0; break; diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 5f4863144..e0b3b70ff 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1961,6 +1961,25 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( widget->isType( UI_TYPE_TEXTSPAN ) && widget->asType()->isInlineBlock() ) widget->asType()->updateLayout(); + + if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed && + widget->getUIStyle() ) { + const StyleSheetProperty* wprop = + widget->getUIStyle()->getProperty( PropertyId::Width ); + if ( wprop && StyleSheetLength::isPercentage( wprop->value() ) ) { + widget->setPixelsSize( { widget->lengthFromValue( *wprop ), + widget->getPixelsSize().getHeight() } ); + } + } + if ( widget->getLayoutHeightPolicy() == SizePolicy::Fixed && + widget->getUIStyle() ) { + const StyleSheetProperty* hprop = + widget->getUIStyle()->getProperty( PropertyId::Height ); + if ( hprop && StyleSheetLength::isPercentage( hprop->value() ) ) { + widget->setPixelsSize( { widget->getPixelsSize().getWidth(), + widget->lengthFromValue( *hprop ) } ); + } + } } Sizef size; diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 20097f4cc..779f00ccd 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -1875,6 +1875,16 @@ std::string UIWidget::getPropertyString( const PropertyDefinition* propertyDef, : "false"; case PropertyId::Focusable: return isTabFocusable() ? "true" : "false"; + case PropertyId::Class: { + std::string cls; + const auto& classes = getStyleSheetClasses(); + for ( size_t i = 0; i < classes.size(); i++ ) { + if ( i > 0 ) + cls += ' '; + cls += classes[i]; + } + return cls; + } default: break; } @@ -2464,8 +2474,9 @@ void UIWidget::loadFromXmlNode( const pugi::xml_node& node ) { } // Create a property without trimming its value - StyleSheetProperty prop( ait->name(), ait->value(), false, - StyleSheetSelectorRule::SpecificityInline ); + Uint64 specificity = + ( mFlags & UI_HTML_ELEMENT ) ? 0 : StyleSheetSelectorRule::SpecificityInline; + StyleSheetProperty prop( ait->name(), ait->value(), false, specificity ); if ( prop.getShorthandDefinition() != NULL ) { auto properties = prop.getShorthandDefinition()->parse( ait->value() ); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 8e05ffcea..a18c80a95 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -3936,3 +3936,142 @@ UTEST( UIHTML, FlexLiItemsWrapContentWidth ) { Engine::destroySingleton(); } + +UTEST( UIHTML, ImagePercentageWidthRespectsParentMaxWidth ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 768, "img pct width respects parent max-width", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + + std::string html; + FileSystem::fileGet( "assets/html/image_width.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + + win->getInput()->update(); + SceneManager::instance()->update(); + sceneNode->updateDirtyLayouts(); + + auto articles = sceneNode->getRoot()->findAllByTag( "article" ); + ASSERT_GT( articles.size(), (size_t)0 ); + auto* articleWidget = articles[0]->asType(); + ASSERT_TRUE( articleWidget != nullptr ); + + auto images = sceneNode->getRoot()->findAllByTag( "img" ); + ASSERT_GT( images.size(), (size_t)0 ); + auto* imgWidget = images[0]->asType(); + ASSERT_TRUE( imgWidget != nullptr ); + + EXPECT_GT( articleWidget->getPixelsSize().getWidth(), 0.f ); + EXPECT_GT( articleWidget->getPixelsSize().getHeight(), 0.f ); + EXPECT_LE( articleWidget->getPixelsSize().getWidth(), 474.f ); + + EXPECT_GT( imgWidget->getPixelsSize().getWidth(), 0.f ); + EXPECT_GT( imgWidget->getPixelsSize().getHeight(), 0.f ); + EXPECT_LE( imgWidget->getPixelsSize().getWidth(), articleWidget->getPixelsSize().getWidth() ); + + auto anchors = sceneNode->getRoot()->findAllByTag( "a" ); + ASSERT_GT( anchors.size(), (size_t)0 ); + auto* anchorWidget = anchors[0]->asType(); + ASSERT_TRUE( anchorWidget != nullptr ); + EXPECT_GT( anchorWidget->getPixelsSize().getWidth(), 0.f ); + EXPECT_GT( anchorWidget->getPixelsSize().getHeight(), 0.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTML, ImageCSSWidthOverridesHTMLWidthAttribute ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 768, "img css width overrides html width attr", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + + std::string html; + FileSystem::fileGet( "assets/html/image_width_2.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + + win->getInput()->update(); + SceneManager::instance()->update(); + sceneNode->updateDirtyLayouts(); + + auto images = sceneNode->getRoot()->findAllByTag( "img" ); + ASSERT_GT( images.size(), (size_t)0 ); + auto* imgWidget = images[0]->asType(); + ASSERT_TRUE( imgWidget != nullptr ); + + auto mains = sceneNode->getRoot()->findAllByTag( "main" ); + ASSERT_GT( mains.size(), (size_t)0 ); + auto* mainWidget = mains[0]->asType(); + ASSERT_TRUE( mainWidget != nullptr ); + + Float imgWidth = imgWidget->getPixelsSize().getWidth(); + Float imgHeight = imgWidget->getPixelsSize().getHeight(); + Float mainWidth = mainWidget->getPixelsSize().getWidth(); + + EXPECT_GT( imgWidth, 0.f ); + EXPECT_GT( imgHeight, 0.f ); + + // CSS width: 100% should override HTML width="1500" + // Image should be no wider than its container + EXPECT_LE( imgWidth, mainWidth ); + + // Image should NOT be 1500 (the HTML attribute value) + EXPECT_LT( imgWidth, 1500.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTML, ImageMaxWidthConstrainsWebpWithHeightAuto ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 768, "img max-width constrains webp with height auto", + WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + + std::string html; + FileSystem::fileGet( "assets/html/image_width_3.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + + win->getInput()->update(); + SceneManager::instance()->update(); + sceneNode->updateDirtyLayouts(); + + auto articles = sceneNode->getRoot()->findAllByTag( "article" ); + ASSERT_GT( articles.size(), (size_t)0 ); + auto* articleWidget = articles[0]->asType(); + ASSERT_TRUE( articleWidget != nullptr ); + + auto images = sceneNode->getRoot()->findAllByTag( "img" ); + ASSERT_GT( images.size(), (size_t)0 ); + auto* imgWidget = images[0]->asType(); + ASSERT_TRUE( imgWidget != nullptr ); + + EXPECT_GT( articleWidget->getPixelsSize().getWidth(), 0.f ); + EXPECT_GT( articleWidget->getPixelsSize().getHeight(), 0.f ); + EXPECT_LE( articleWidget->getPixelsSize().getWidth(), 1140.f ); + + Float imgWidth = imgWidget->getPixelsSize().getWidth(); + Float imgHeight = imgWidget->getPixelsSize().getHeight(); + + EXPECT_GT( imgWidth, 0.f ); + EXPECT_GT( imgHeight, 0.f ); + EXPECT_LE( imgWidth, articleWidget->getPixelsSize().getWidth() ); + + // max-width: 100% should prevent the image from using the HTML width="2560" + EXPECT_LT( imgWidth, 2560.f ); + + // height should be proportional to width (aspect ratio: 1436/2560) + Float expectedRatio = 1436.f / 2560.f; + Float actualRatio = imgHeight / imgWidth; + EXPECT_GT( actualRatio, expectedRatio * 0.9f ); + EXPECT_LT( actualRatio, expectedRatio * 1.1f ); + + Engine::destroySingleton(); +}