diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index 8472c26c7..d71fd0af3 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -128,6 +128,7 @@ enum UINodeType { UI_TYPE_DIFF_VIEW, UI_TYPE_BR, UI_TYPE_HTML_HTML, + UI_TYPE_HTML_HEAD, UI_TYPE_HTML_BODY, UI_TYPE_HTML_LIST_ITEM, UI_TYPE_HTML_IMAGE, diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 7d489bc46..37a5ab31f 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -11,7 +11,13 @@ class EE_API UIRichText : public UIHTMLWidget { public: enum class IntrinsicMode { None, Min, Max }; - enum class WhiteSpaceCollapse { Collapse, Preserve, PreserveBreaks, PreserveSpaces, BreakSpaces }; + enum class WhiteSpaceCollapse { + Collapse, + Preserve, + PreserveBreaks, + PreserveSpaces, + BreakSpaces + }; static WhiteSpaceCollapse toWhiteSpaceCollapse( std::string val ); @@ -213,6 +219,16 @@ class EE_API UIHTMLBody : public UIRichText { UIHTMLBody( const std::string& tag = "body" ); }; +class EE_API UIHTMLHead : public UIWidget { + public: + static UIHTMLHead* New(); + virtual Uint32 getType() const override; + bool isType( const Uint32& type ) const override; + + protected: + UIHTMLHead(); +}; + class EE_API UILineBreak : public UIRichText { public: static UILineBreak* New( const std::string& tag ); diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index fdfb08904..a4153f4fe 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -211,6 +211,23 @@ void UIHTMLBody::updateLayout() { } } +UIHTMLHead* UIHTMLHead::New() { + return eeNew( UIHTMLHead, () ); +} + +UIHTMLHead::UIHTMLHead() : UIWidget() { + mVisible = false; + mEnabled = false; +} + +Uint32 UIHTMLHead::getType() const { + return UI_TYPE_HTML_HEAD; +} + +bool UIHTMLHead::isType( const Uint32& type ) const { + return UIHTMLHead::getType() == type ? true : UIWidget::isType( type ); +} + UIRichText* UIRichText::NewHtml() { auto* html = UIHTMLHtml::New( "html" ); html->setClipType( ClipType::None ); @@ -1028,6 +1045,10 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri UIWidget* widget = node->asType(); + // Skip - it must not participate in layout + if ( widget->isType( UI_TYPE_HTML_HEAD ) ) + return; + bool handled = false; if ( widget->isType( UI_TYPE_HTML_WIDGET ) && widget->asType()->isInline() ) { diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index 265a8160c..7222294ee 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -236,7 +236,7 @@ void UIWidgetCreator::createBaseWidgetList() { }; registeredWidget["aside"] = [] { return UIRichText::NewWithTag( "aside" ); }; registeredWidget["html"] = UIRichText::NewHtml; - registeredWidget["head"] = [] { return UIWidget::NewWithTag( "head" ); }; + registeredWidget["head"] = UIHTMLHead::New; registeredWidget["body"] = UIRichText::NewBody; registeredWidget["form"] = [] { return UIHTMLForm::New(); }; registeredWidget["table"] = UIHTMLTable::New; diff --git a/src/tests/unit_tests/uitextnode_tests.cpp b/src/tests/unit_tests/uitextnode_tests.cpp index 575e53c64..33ac8ebab 100644 --- a/src/tests/unit_tests/uitextnode_tests.cpp +++ b/src/tests/unit_tests/uitextnode_tests.cpp @@ -30,6 +30,7 @@ using namespace EE::Window; using namespace EE::Scene; using namespace EE::UI; using namespace EE::UI::CSS; +using namespace EE::UI::Tools; // Helper: create a basic scene for RichText tests static UI::UISceneNode* createRichTextScene() { @@ -879,8 +880,7 @@ UTEST( UITextNode_BlockLayouter, OverFindHitsAnchorWhenMatchingText ) { if ( !anchor->getHitBoxes().empty() ) { const Rectf& firstHb = anchor->getHitBoxes()[0]; - Vector2f hitPos = anchor->convertToWorldSpace( - { firstHb.Left + 1, firstHb.Top + 1 } ); + Vector2f hitPos = anchor->convertToWorldSpace( { firstHb.Left + 1, firstHb.Top + 1 } ); Node* hitNode = rt->overFind( hitPos ); EXPECT_EQ( hitNode, anchor ); } @@ -914,8 +914,7 @@ UTEST( UITextNode_BlockLayouter, NestedSpanOverFindHitsInnerSpan ) { if ( !inner->getHitBoxes().empty() ) { const Rectf& firstHb = inner->getHitBoxes()[0]; - Vector2f hitPos = inner->convertToWorldSpace( - { firstHb.Left + 1, firstHb.Top + 1 } ); + Vector2f hitPos = inner->convertToWorldSpace( { firstHb.Left + 1, firstHb.Top + 1 } ); Node* hitNode = rt->overFind( hitPos ); EXPECT_TRUE( hitNode == inner || hitNode->inParentTreeOf( inner ) ); } @@ -994,6 +993,49 @@ UTEST( UITextNode_EdgeCases, DirectChildOfRichText ) { // Suite: UITextNode_RegressionTests // ============================================================ +UTEST( UITextNode_Regression, BackgroundPositioningBodyYWithLineHeight ) { + auto sceneNode = createRichTextScene(); + ASSERT_TRUE( sceneNode != nullptr ); + + String xml = R"xml( + + + + + + + + +)xml"; + + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( xml ) ); + sceneNode->update( Time::Zero ); + + auto* body = sceneNode->find( "body" ); + ASSERT_TRUE( body != nullptr ); + + // Body must be at Y=0 - the element should NOT participate in layout + // and should NOT create an empty line with line-height applied. + EXPECT_NEAR( body->getPixelsPosition().y, 0.f, 1.f ); + + destroyRichTextScene( sceneNode ); +} + UTEST( UITextNode_Regression, WhitespaceCollapseDoesNotCreateSpuriousNodes ) { auto sceneNode = createRichTextScene(); ASSERT_TRUE( sceneNode != nullptr );