From ae0fd6bc2b7be81318e23dabd47eec82f1479d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Wed, 29 Apr 2026 13:05:29 -0300 Subject: [PATCH] Fixes for absolute positioning and some minor details. --- .../assets/html/absolute_position.html | 54 +++++++++++++++ include/eepp/ui/uihtmltable.hpp | 2 - include/eepp/ui/uihtmlwidget.hpp | 4 ++ include/eepp/ui/uilayout.hpp | 5 +- include/eepp/ui/uirichtext.hpp | 2 - src/eepp/ui/css/stylesheetspecification.cpp | 16 +++-- src/eepp/ui/uihtmltable.cpp | 10 --- src/eepp/ui/uihtmlwidget.cpp | 54 ++++++++------- src/eepp/ui/uirichtext.cpp | 23 +++---- src/examples/ui_html/ui_html.cpp | 21 +++++- .../unit_tests/uihtml_position_tests.cpp | 67 +++++++++++++++++++ 11 files changed, 197 insertions(+), 61 deletions(-) create mode 100644 bin/unit_tests/assets/html/absolute_position.html diff --git a/bin/unit_tests/assets/html/absolute_position.html b/bin/unit_tests/assets/html/absolute_position.html new file mode 100644 index 000000000..07a71e8ac --- /dev/null +++ b/bin/unit_tests/assets/html/absolute_position.html @@ -0,0 +1,54 @@ + + + + + + +
+
+
File Upload
+ +
+
+
Download Files
+
+ ENTER +
+
+
+ + \ No newline at end of file diff --git a/include/eepp/ui/uihtmltable.hpp b/include/eepp/ui/uihtmltable.hpp index 3cfe4b669..8d2e094d0 100644 --- a/include/eepp/ui/uihtmltable.hpp +++ b/include/eepp/ui/uihtmltable.hpp @@ -18,8 +18,6 @@ class EE_API UIHTMLTable : public UIHTMLWidget { virtual bool isType( const Uint32& type ) const; - virtual void updateLayout(); - virtual Float getMinIntrinsicWidth() const; virtual Float getMaxIntrinsicWidth() const; diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index 1b6d077e1..2a750ee4a 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -61,6 +61,10 @@ class EE_API UIHTMLWidget : public UILayout { protected: CSSDisplay mDisplay{ CSSDisplay::Block }; CSSPosition mPosition{ CSSPosition::Static }; + std::string mTopEq{ "auto" }; + std::string mRightEq{ "auto" }; + std::string mBottomEq{ "auto" }; + std::string mLeftEq{ "auto" }; Rectf mOffsets{ 0, 0, 0, 0 }; int mZIndex{ 0 }; UILayouter* mLayouter{ nullptr }; diff --git a/include/eepp/ui/uilayout.hpp b/include/eepp/ui/uilayout.hpp index ad1c7714c..0217a8476 100644 --- a/include/eepp/ui/uilayout.hpp +++ b/include/eepp/ui/uilayout.hpp @@ -24,6 +24,9 @@ class EE_API UILayout : public UIWidget { bool isLayoutDirty() const { return mDirtyLayout; } void onAutoSizeChild( UIWidget* child ); + + void setLayoutDirty(); + protected: friend class UISceneNode; friend class UILayouter; @@ -51,8 +54,6 @@ class EE_API UILayout : public UIWidget { virtual void updateLayoutWrappingContents(); - void setLayoutDirty(); - bool setMatchParentIfNeededVerticalGrowth(); }; diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index f87b92f1a..6941c7bab 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -129,8 +129,6 @@ class EE_API UIRichText : public UIHTMLWidget { String getSelectionString() const; - virtual void updateLayout(); - virtual RichText* getRichTextPtr() { return &mRichText; } protected: diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 5cf4ee98d..837f25f02 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -431,10 +431,18 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "list-style-type", "none", true ).setType( PropertyType::String ); registerProperty( "list-style-position", "outside", true ).setType( PropertyType::String ); registerProperty( "list-style-image", "none" ).setType( PropertyType::String ); - registerProperty( "top", "auto" ).setType( PropertyType::NumberLength ); - registerProperty( "right", "auto" ).setType( PropertyType::NumberLength ); - registerProperty( "bottom", "auto" ).setType( PropertyType::NumberLength ); - registerProperty( "left", "auto" ).setType( PropertyType::NumberLength ); + registerProperty( "top", "auto" ) + .setType( PropertyType::NumberLength ) + .setRelativeTarget( PropertyRelativeTarget::ContainingBlockHeight ); + registerProperty( "right", "auto" ) + .setType( PropertyType::NumberLength ) + .setRelativeTarget( PropertyRelativeTarget::ContainingBlockWidth ); + registerProperty( "bottom", "auto" ) + .setType( PropertyType::NumberLength ) + .setRelativeTarget( PropertyRelativeTarget::ContainingBlockHeight ); + registerProperty( "left", "auto" ) + .setType( PropertyType::NumberLength ) + .setRelativeTarget( PropertyRelativeTarget::ContainingBlockWidth ); registerProperty( "z-index", "auto" ).setType( PropertyType::NumberInt ); registerProperty( "inner-widget-orientation", "widgeticontextbox" ) diff --git a/src/eepp/ui/uihtmltable.cpp b/src/eepp/ui/uihtmltable.cpp index 123b18a6e..f71b2c8f3 100644 --- a/src/eepp/ui/uihtmltable.cpp +++ b/src/eepp/ui/uihtmltable.cpp @@ -85,16 +85,6 @@ Float UIHTMLTable::getMaxIntrinsicWidth() const { return 0; } -void UIHTMLTable::updateLayout() { - UILayouter* layouter = const_cast( this )->getLayouter(); - if ( layouter ) - getLayouter()->updateLayout(); - else - UIHTMLWidget::updateLayout(); - - mDirtyLayout = false; -} - Uint32 UIHTMLTable::onMessage( const NodeMessage* Msg ) { switch ( Msg->getMsg() ) { case NodeMessage::LayoutAttributeChange: { diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index 6e96404df..58157b6c2 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -60,6 +60,10 @@ void UIHTMLWidget::setCSSPosition( CSSPosition position ) { void UIHTMLWidget::setOffsets( const Rectf& offsets ) { if ( mOffsets != offsets ) { mOffsets = offsets; + mTopEq = String::fromFloat( offsets.Top, "dp" ); + mLeftEq = String::fromFloat( offsets.Left, "dp" ); + mRightEq = String::fromFloat( offsets.Right, "dp" ); + mBottomEq = String::fromFloat( offsets.Bottom, "dp" ); notifyLayoutAttrChange(); } } @@ -79,13 +83,13 @@ std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyD case PropertyId::Position: return CSSPositionHelper::toString( mPosition ); case PropertyId::Top: - return String::fromFloat( mOffsets.Top, "dp" ); + return mTopEq; case PropertyId::Right: - return String::fromFloat( mOffsets.Right, "dp" ); + return mRightEq; case PropertyId::Bottom: - return String::fromFloat( mOffsets.Bottom, "dp" ); + return mBottomEq; case PropertyId::Left: - return String::fromFloat( mOffsets.Left, "dp" ); + return mLeftEq; case PropertyId::ZIndex: return String::toString( mZIndex ); default: @@ -111,34 +115,22 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) { return true; } case PropertyId::Top: { - if ( attribute.asString() == "auto" ) - mOffsets.Top = 0; - else - mOffsets.Top = lengthFromValueAsDp( attribute ); + mTopEq = attribute.asString(); notifyLayoutAttrChange(); return true; } case PropertyId::Right: { - if ( attribute.asString() == "auto" ) - mOffsets.Right = 0; - else - mOffsets.Right = lengthFromValueAsDp( attribute ); + mRightEq = attribute.asString(); notifyLayoutAttrChange(); return true; } case PropertyId::Bottom: { - if ( attribute.asString() == "auto" ) - mOffsets.Bottom = 0; - else - mOffsets.Bottom = lengthFromValueAsDp( attribute ); + mBottomEq = attribute.asString(); notifyLayoutAttrChange(); return true; } case PropertyId::Left: { - if ( attribute.asString() == "auto" ) - mOffsets.Left = 0; - else - mOffsets.Left = lengthFromValueAsDp( attribute ); + mLeftEq = attribute.asString(); notifyLayoutAttrChange(); return true; } @@ -148,7 +140,6 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) { return UILayout::applyProperty( attribute ); } - void UIHTMLWidget::updateLayout() { if ( getLayouter() ) getLayouter()->updateLayout(); @@ -156,6 +147,8 @@ void UIHTMLWidget::updateLayout() { UILayout::updateLayout(); positionOutOfFlowChildren(); + + mDirtyLayout = false; } UIWidget* UIHTMLWidget::getContainingBlock() { @@ -195,11 +188,22 @@ void UIHTMLWidget::positionOutOfFlowChildren() { if ( pos == CSSPosition::Absolute || pos == CSSPosition::Fixed ) { UIWidget* cb = htmlChild->getContainingBlock(); if ( cb ) { - Rectf offsets = htmlChild->getOffsets(); - Float top = PixelDensity::dpToPx( offsets.Top ); - Float left = PixelDensity::dpToPx( offsets.Left ); + Float top = htmlChild->mTopEq == "auto" + ? 0 + : htmlChild->lengthFromValue( + htmlChild->mTopEq, + CSS::PropertyRelativeTarget::ContainingBlockHeight, 0 ); + Float left = htmlChild->mLeftEq == "auto" + ? 0 + : htmlChild->lengthFromValue( + htmlChild->mLeftEq, + CSS::PropertyRelativeTarget::ContainingBlockWidth, 0 ); - Vector2f cbPos( cb->getPixelsContentOffset().Left, cb->getPixelsContentOffset().Top ); + top += htmlChild->getLayoutPixelsMargin().Top; + left += htmlChild->getLayoutPixelsMargin().Left; + + Vector2f cbPos( cb->getPixelsContentOffset().Left, + cb->getPixelsContentOffset().Top ); cbPos.x += left; cbPos.y += top; diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index a7f471589..14e9edcd6 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #define PUGIXML_HEADER_ONLY #include @@ -94,11 +93,15 @@ bool UIHTMLBody::applyProperty( const StyleSheetProperty& attribute ) { } UIRichText* UIRichText::NewHtml() { - return UIHTMLHtml::New( "html" ); + auto* html = UIHTMLHtml::New( "html" ); + html->setClipType( ClipType::None ); + return html; } UIRichText* UIRichText::NewBody() { - return UIHTMLBody::New( "body" ); + auto* body = UIHTMLBody::New( "body" ); + body->setClipType( ClipType::None ); + return body; } UIRichText* UIRichText::NewBr() { @@ -261,7 +264,7 @@ bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) { break; } default: - return UILayout::applyProperty( attribute ); + return UIHTMLWidget::applyProperty( attribute ); } return true; @@ -305,7 +308,7 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef ? "center" : ( getTextAlign() == TEXT_ALIGN_RIGHT ? "right" : "left" ); default: - return UILayout::getPropertyString( propertyDef, propertyIndex ); + return UIHTMLWidget::getPropertyString( propertyDef, propertyIndex ); } } @@ -701,16 +704,6 @@ void UIRichText::updateDefaultSpansStyle() { } } -void UIRichText::updateLayout() { - if ( getLayouter() ) { - getLayouter()->updateLayout(); - } else { - UILayout::updateLayout(); - } - - mDirtyLayout = false; -} - Float UIRichText::getMinIntrinsicWidth() const { if ( mWidthPolicy == SizePolicy::Fixed ) { return getPropertyWidth(); diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp index ac86ca78b..4d1046afd 100644 --- a/src/examples/ui_html/ui_html.cpp +++ b/src/examples/ui_html/ui_html.cpp @@ -82,6 +82,7 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { auto urlBar = ui->find( "url_bar" )->asType(); auto mainContainer = ui->find( "html_doc" ); + mainContainer->asType()->setClipType( ClipType::None ); auto backBtn = ui->find( "backbtn" )->asType(); auto fwdBtn = ui->find( "fwdbtn" )->asType(); auto scrollView = ui->find( "html_view" )->asType(); @@ -98,13 +99,31 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { if ( data.empty() ) return; ui->ensureMainThread( [url, data, mainContainer, urlBar, ui, &app, scrollView, useHNDark] { + scrollView->removeEventsOfType( Event::OnSizeChange ); mainContainer->closeAllChildren(); + scrollView->getVerticalScrollBar()->setValue( 0 ); ui->getStyleSheet().removeAllWithoutMarker( app.getStyleSheetDefaultMarker() ); ui->setURIFromURL( url ); auto urlStr = url.toString(); auto hash = String::hash( urlStr ); - scrollView->getVerticalScrollBar()->setValue( 0 ); ui->loadLayoutFromString( HTMLFormatter::HTMLtoXML( data ), mainContainer, hash ); + auto htmlNode = ui->findByType( UI_TYPE_HTML_HTML ); + auto bodyNode = ui->findByType( UI_TYPE_HTML_BODY ); + if ( htmlNode && bodyNode ) { + auto html = htmlNode->asType(); + auto body = bodyNode->asType(); + html->setMinHeight( scrollView->getPixelsSize().getHeight() ); + body->setMinHeight( scrollView->getPixelsSize().getHeight() ); + scrollView->on( Event::OnSizeChange, [scrollView, html, body]( auto ) { + body->setMinHeight( scrollView->getSize().getHeight() ); + body->setPixelsSize( { html->getPixelsSize().getWidth(), 0 } ); + html->setMinHeight( scrollView->getSize().getHeight() ); + html->setPixelsSize( { html->getPixelsSize().getWidth(), 0 } ); + } ); + body->on( Event::OnClose, [scrollView]( auto ) { + scrollView->removeEventsOfType( Event::OnSizeChange ); + } ); + } urlBar->setText( urlStr ); if ( useHNDark && url.getAuthority() == "news.ycombinator.com" ) { diff --git a/src/tests/unit_tests/uihtml_position_tests.cpp b/src/tests/unit_tests/uihtml_position_tests.cpp index 28c4dccf7..a8554c750 100644 --- a/src/tests/unit_tests/uihtml_position_tests.cpp +++ b/src/tests/unit_tests/uihtml_position_tests.cpp @@ -196,3 +196,70 @@ UTEST( UIHTMLWidget, positionOutOfFlow_DoesNotAffectParentSize ) { Engine::destroySingleton(); } + +UTEST( UIHTMLWidget, positionOutOfFlow_PercentageAndMargin ) { + init_ui_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIHTMLWidget* rootContainer = UIHTMLWidget::New(); + rootContainer->setParent( sceneNode->getRoot() ); + rootContainer->setCSSPosition( CSSPosition::Relative ); + rootContainer->setPixelsSize( 800, 600 ); + rootContainer->setPixelsPosition( 0, 0 ); + + UIHTMLWidget* absoluteChild = UIHTMLWidget::New(); + absoluteChild->setParent( rootContainer ); + absoluteChild->setPixelsSize( 400, 400 ); + + // Emulate the CSS parsing via applyProperty + absoluteChild->applyProperty( StyleSheetProperty( "position", "absolute" ) ); + absoluteChild->applyProperty( StyleSheetProperty( "left", "50%" ) ); + absoluteChild->applyProperty( StyleSheetProperty( "margin-left", "-200px" ) ); + absoluteChild->applyProperty( StyleSheetProperty( "margin-top", "120px" ) ); + + sceneNode->updateDirtyLayouts(); + + UIWidget* cb = absoluteChild->getContainingBlock(); + EXPECT_EQ( cb, rootContainer ); + + Vector2f worldPos = absoluteChild->convertToWorldSpace( { 0, 0 } ); + // left should be 50% of 800 (400) plus margin-left (-200) = 200 + // top should be 0 + margin-top (120) = 120 + EXPECT_NEAR( 200.f, worldPos.x, 1.f ); + EXPECT_NEAR( 120.f, worldPos.y, 1.f ); + + Engine::destroySingleton(); +} + +#include +#include + +UTEST( UIHTMLWidget, positionOutOfFlow_ComplexHTML ) { + init_ui_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + std::string html; + FileSystem::fileGet( "assets/html/absolute_position.html", html ); + std::string xml = UI::Tools::HTMLFormatter::HTMLtoXML( html ); + sceneNode->loadLayoutFromString( xml ); + + sceneNode->update( Milliseconds( 16 ) ); + sceneNode->updateDirtyLayouts(); + + UIWidget* mainWidget = sceneNode->getRoot()->find( "main" ); + ASSERT_TRUE( mainWidget != nullptr ); + EXPECT_GT( mainWidget->getPixelsSize().getHeight(), 0.f ); // This is not standard in HTML! + + Vector2f worldPos = mainWidget->convertToWorldSpace( { 0, 0 } ); + // Window size is 1024x650 + // left: 50% of 1024 = 512 + // margin-left: -200px + // 512 - 200 = 312 + EXPECT_NEAR( 312.f, worldPos.x, 1.f ); + + // top should just be margin-top + EXPECT_NEAR( 120.f, worldPos.y, 1.f ); + + Engine::destroySingleton(); +}