diff --git a/bin/unit_tests/assets/html/blog_main_incorrect_widths.html b/bin/unit_tests/assets/html/blog_main_incorrect_widths.html
new file mode 100644
index 000000000..3372c0f55
--- /dev/null
+++ b/bin/unit_tests/assets/html/blog_main_incorrect_widths.html
@@ -0,0 +1,376 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Personal pieces of rambling reflections and rants, tangential thoughts and tirades.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp
index f087fed29..4cfba07cd 100644
--- a/include/eepp/graphics/richtext.hpp
+++ b/include/eepp/graphics/richtext.hpp
@@ -77,7 +77,12 @@ class EE_API RichText : public Drawable {
enum class BlockType { Text, Drawable, CustomSize };
- using Block = std::variant, std::shared_ptr, Sizef>;
+ struct CustomBlock {
+ Sizef size;
+ bool isBlock{ false };
+ };
+
+ using Block = std::variant, std::shared_ptr, CustomBlock>;
/**
* @brief Adds a drawable (e.g., an image) into the text flow.
@@ -88,8 +93,9 @@ class EE_API RichText : public Drawable {
/**
* @brief Adds a custom size spacer into the text flow.
* @param size The physical dimensions of the spacer.
+ * @param isBlock Whether this spacer acts as a block-level element.
*/
- void addCustomSize( const Sizef& size );
+ void addCustomSize( const Sizef& size, bool isBlock = false );
/** @return The list of blocks. */
const std::vector& getBlocks() { return mBlocks; }
diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp
index f470f9f0a..1bacef8cd 100644
--- a/src/eepp/graphics/richtext.cpp
+++ b/src/eepp/graphics/richtext.cpp
@@ -95,7 +95,7 @@ void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, cons
span.size );
}
},
- []( const Sizef& ) {} },
+ []( const CustomBlock& ) {} },
span.block );
}
}
@@ -282,8 +282,8 @@ void RichText::addDrawable( std::shared_ptr drawable ) {
invalidateLayout();
}
-void RichText::addCustomSize( const Sizef& size ) {
- mBlocks.push_back( size );
+void RichText::addCustomSize( const Sizef& size, bool isBlock ) {
+ mBlocks.push_back( CustomBlock{ size, isBlock } );
invalidateLayout();
}
@@ -365,8 +365,8 @@ Float RichText::getMinIntrinsicWidth() {
}
} else if ( auto pDrawable = std::get_if>( &block ) ) {
minW = std::max( minW, ( *pDrawable )->getPixelsSize().getWidth() );
- } else if ( auto pSize = std::get_if( &block ) ) {
- minW = std::max( minW, pSize->getWidth() );
+ } else if ( auto pSize = std::get_if( &block ) ) {
+ minW = std::max( minW, pSize->size.getWidth() );
}
}
return minW;
@@ -395,8 +395,16 @@ Float RichText::getMaxIntrinsicWidth() {
span->getTextHints() );
} else if ( auto pDrawable = std::get_if>( &block ) ) {
curX += ( *pDrawable )->getPixelsSize().getWidth();
- } else if ( auto pSize = std::get_if( &block ) ) {
- curX += pSize->getWidth();
+ } else if ( auto pSize = std::get_if( &block ) ) {
+ if ( pSize->isBlock ) {
+ if ( curX > 0 ) {
+ maxW = std::max( maxW, curX );
+ curX = 0;
+ }
+ maxW = std::max( maxW, pSize->size.getWidth() );
+ } else {
+ curX += pSize->size.getWidth();
+ }
}
}
maxW = std::max( maxW, curX );
@@ -484,15 +492,23 @@ void RichText::updateLayout() {
}
} else { // Drawable or CustomSize
Sizef blockSize;
+ bool isBlock = false;
if ( auto pDrawable = std::get_if>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
- } else if ( auto pSize = std::get_if( &block ) ) {
- blockSize = *pSize;
+ } else if ( auto pSize = std::get_if( &block ) ) {
+ blockSize = pSize->size;
+ isBlock = pSize->isBlock;
+ }
+
+ if ( isBlock && curX > 0 ) {
+ maxWidth = std::max( maxWidth, curX );
+ mLines.push_back( RenderParagraph() );
+ curX = 0;
}
// Wrap if needed
- if ( mMaxWidth > 0 &&
+ if ( mMaxWidth > 0 && !isBlock &&
( curX + blockSize.getWidth() >= mMaxWidth || curX >= mMaxWidth ) && curX > 0 ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
@@ -516,7 +532,7 @@ void RichText::updateLayout() {
curX += blockSize.getWidth();
currentLine.width += blockSize.getWidth();
- if ( mMaxWidth > 0 && curX >= mMaxWidth ) {
+ if ( ( mMaxWidth > 0 && curX >= mMaxWidth ) || isBlock ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp
index 443d3f184..6251bb573 100644
--- a/src/eepp/ui/uirichtext.cpp
+++ b/src/eepp/ui/uirichtext.cpp
@@ -17,7 +17,7 @@ namespace EE { namespace UI {
class UILineBreak : public UIRichText {
public:
- static UILineBreak* New( const std::string& tag = "" ) { return eeNew( UILineBreak, ( tag ) ); }
+ static UILineBreak* New( const std::string& tag ) { return eeNew( UILineBreak, ( tag ) ); }
UILineBreak( const std::string& tag = "br" ) : UIRichText( tag ) {}
@@ -29,7 +29,7 @@ class UILineBreak : public UIRichText {
};
UIRichText* UIRichText::NewBr() {
- return UILineBreak::New();
+ return UILineBreak::New( "br" );
};
UIRichText* UIRichText::NewHr() {
@@ -490,18 +490,14 @@ void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) {
mw = 0.f;
}
- if ( mWidthPolicy == SizePolicy::WrapContent || mode != IntrinsicMode::None ) {
- if ( mode == IntrinsicMode::None && !mMaxWidthEq.empty() ) {
- richText.setMaxWidth( mw );
- } else {
- richText.setMaxWidth( 0.f ); // Let it grow unbounded to query text bounds later
- }
- } else {
- if ( !mMaxWidthEq.empty() && mw < maxWidth ) {
+ if ( mode == IntrinsicMode::None ) {
+ if ( !mMaxWidthEq.empty() && ( maxWidth == 0 || mw < maxWidth ) ) {
richText.setMaxWidth( mw );
} else {
richText.setMaxWidth( maxWidth );
}
+ } else {
+ richText.setMaxWidth( 0.f ); // Let it grow unbounded to query text bounds later
}
auto processWidget = [&]( UIWidget* widget, auto& processWidgetRef ) -> void {
@@ -577,7 +573,7 @@ void UIRichText::positionChildren() {
while ( currentSpan < line.spans.size() ) {
const auto& span = line.spans[currentSpan];
currentSpan++;
- if ( std::holds_alternative( span.block ) )
+ if ( std::holds_alternative( span.block ) )
return &span;
}
currentSpan = 0;
diff --git a/src/tests/unit_tests/richtext.cpp b/src/tests/unit_tests/richtext.cpp
index 61d3ff169..e82ab19cd 100644
--- a/src/tests/unit_tests/richtext.cpp
+++ b/src/tests/unit_tests/richtext.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -22,6 +23,7 @@ using namespace EE::Graphics;
using namespace EE::Window;
using namespace EE::Scene;
using namespace EE::UI;
+using namespace EE::UI::Tools;
UTEST( RichText, basicFunctionality ) {
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test",
@@ -365,8 +367,9 @@ UTEST( UIRichText, IntegrationAndLayoutVerification ) {
EXPECT_TRUE( text1->getFillColor() == Color::fromString( "#FF0000" ) );
// Check CustomSize block
- EXPECT_TRUE( std::holds_alternative( blocks[2] ) );
- EXPECT_EQ( std::get( blocks[2] ).getWidth(), PixelDensity::dpToPx( 50 ) );
+ EXPECT_TRUE( std::holds_alternative( blocks[2] ) );
+ EXPECT_EQ( std::get( blocks[2] ).size.getWidth(),
+ PixelDensity::dpToPx( 50 ) );
UI::UIWidget* placeholder = rt->find( "placeholder" );
ASSERT_TRUE( placeholder != nullptr );
@@ -463,10 +466,11 @@ UTEST( UIRichText, NestedWidgetsIntegration ) {
// Check block types
EXPECT_TRUE( std::holds_alternative>( blocks[0] ) );
EXPECT_TRUE( std::holds_alternative>( blocks[1] ) );
- EXPECT_TRUE( std::holds_alternative( blocks[2] ) );
+ EXPECT_TRUE( std::holds_alternative( blocks[2] ) );
EXPECT_TRUE( std::holds_alternative>( blocks[3] ) );
- EXPECT_EQ( std::get( blocks[2] ).getWidth(), PixelDensity::dpToPx( 50 ) );
+ EXPECT_EQ( std::get( blocks[2] ).size.getWidth(),
+ PixelDensity::dpToPx( 50 ) );
UI::UIWidget* strongNode = rt->find( "strong" );
ASSERT_TRUE( strongNode != nullptr );
@@ -1059,18 +1063,20 @@ UTEST( UIRichText, MinMaxWidth ) {
EXPECT_EQ( rtMin->getSize().getWidth(), PixelDensity::dpToPx( 200 ) );
EXPECT_LE( rtMax->getSize().getWidth(), PixelDensity::dpToPx( 100 ) );
- EXPECT_GT( rtMax->getSize().getHeight(), PixelDensity::dpToPx( 30 ) ); // should wrap to multiple lines
+ EXPECT_GT( rtMax->getSize().getHeight(),
+ PixelDensity::dpToPx( 30 ) ); // should wrap to multiple lines
EXPECT_LE( rtMaxFixed->getSize().getWidth(), PixelDensity::dpToPx( 100 ) );
- EXPECT_GT( rtMaxFixed->getSize().getHeight(), PixelDensity::dpToPx( 30 ) ); // should wrap to multiple lines
+ EXPECT_GT( rtMaxFixed->getSize().getHeight(),
+ PixelDensity::dpToPx( 30 ) ); // should wrap to multiple lines
eeDelete( sceneNode );
Engine::destroySingleton();
}
UTEST( UIRichText, MinMaxWidthChildren ) {
- Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Min/Max Width Children Test",
- WindowStyle::Default, WindowBackend::Default,
- 32, {}, 1, false, true ) );
+ Engine::instance()->createWindow(
+ WindowSettings( 800, 600, "RichText Min/Max Width Children Test", WindowStyle::Default,
+ WindowBackend::Default, 32, {}, 1, false, true ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
@@ -1098,7 +1104,8 @@ UTEST( UIRichText, MinMaxWidthChildren ) {
ASSERT_TRUE( childWidget != nullptr );
sceneNode->update( Time::Zero );
- sceneNode->update( Time::Zero ); // Run a second pass to allow MatchParent to resolve against the new clamped parent size
+ sceneNode->update( Time::Zero ); // Run a second pass to allow MatchParent to resolve against
+ // the new clamped parent size
sceneNode->update( Time::Zero );
EXPECT_LE( rtParent->getSize().getWidth(), PixelDensity::dpToPx( 100 ) );
@@ -1112,9 +1119,9 @@ UTEST( UIRichText, MinMaxWidthChildren ) {
}
UTEST( UIRichText, MatchParentChildPadding ) {
- Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText MatchParent Child Padding Test",
- WindowStyle::Default, WindowBackend::Default,
- 32, {}, 1, false, true ) );
+ Engine::instance()->createWindow(
+ WindowSettings( 800, 600, "RichText MatchParent Child Padding Test", WindowStyle::Default,
+ WindowBackend::Default, 32, {}, 1, false, true ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
@@ -1153,9 +1160,9 @@ UTEST( UIRichText, MatchParentChildPadding ) {
}
UTEST( UILayout, MinMaxWidthChildren ) {
- Engine::instance()->createWindow( WindowSettings( 800, 600, "Layout Min/Max Width Children Test",
- WindowStyle::Default, WindowBackend::Default,
- 32, {}, 1, false, true ) );
+ Engine::instance()->createWindow(
+ WindowSettings( 800, 600, "Layout Min/Max Width Children Test", WindowStyle::Default,
+ WindowBackend::Default, 32, {}, 1, false, true ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
@@ -1195,3 +1202,180 @@ UTEST( UILayout, MinMaxWidthChildren ) {
eeDelete( sceneNode );
Engine::destroySingleton();
}
+
+UTEST( UIRichText, InvalidWidthLengthComputation ) {
+ Engine::instance()->createWindow( WindowSettings( 800, 600, "Invalid Anchor Width",
+ WindowStyle::Default, WindowBackend::Default,
+ 32, {}, 1, false, true ) );
+ FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
+
+ FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
+ font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
+ ASSERT_TRUE( font->loaded() );
+ FontFamily::loadFromRegular( font );
+
+ UISceneNode* sceneNode = UISceneNode::New();
+ UIThemeManager* themeManager = sceneNode->getUIThemeManager();
+ themeManager->setDefaultFont( font );
+
+ String xml = R"xml(
+
+ )xml";
+
+ String html = R"html(
+
+
+
+
+
+
+ )html";
+
+ sceneNode->loadLayoutFromString( xml );
+ auto htmlView = sceneNode->find( "html_view" );
+ ASSERT_TRUE( htmlView != nullptr );
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), htmlView );
+ auto parent = sceneNode->find( "anchor_parent" );
+ auto anchor = sceneNode->find( "anchor" );
+ ASSERT_TRUE( parent != nullptr );
+ ASSERT_TRUE( anchor != nullptr );
+
+ sceneNode->update( Time::Zero );
+
+ EXPECT_LE( anchor->getSize().getWidth(), parent->getSize().getWidth() );
+
+ eeDelete( sceneNode );
+ Engine::destroySingleton();
+}
+
+UTEST( UIRichText, InvalidWidthLengthComputation2 ) {
+ Engine::instance()->createWindow( WindowSettings( 800, 600, "Invalid Anchor Width 2",
+ WindowStyle::Default, WindowBackend::Default,
+ 32, {}, 1, false, true ) );
+ FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
+
+ FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
+ font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
+ ASSERT_TRUE( font->loaded() );
+ FontFamily::loadFromRegular( font );
+
+ UISceneNode* sceneNode = UISceneNode::New();
+ UIThemeManager* themeManager = sceneNode->getUIThemeManager();
+ themeManager->setDefaultFont( font );
+
+ String xml = R"xml(
+
+ )xml";
+
+ String html = R"html(
+
+
+
+
+
+ )html";
+
+ sceneNode->loadLayoutFromString( xml );
+ auto htmlView = sceneNode->find( "html_view" );
+ ASSERT_TRUE( htmlView != nullptr );
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), htmlView );
+ auto parent = sceneNode->find( "anchor_parent" );
+ auto anchor = sceneNode->find( "anchor" );
+ auto anchorDiv = sceneNode->find( "anchor_div" );
+ auto anchorH2 = sceneNode->find( "anchor_h2" );
+ auto anchorSpan = sceneNode->find( "anchor_span" );
+ ASSERT_TRUE( parent != nullptr );
+ ASSERT_TRUE( anchor != nullptr );
+ ASSERT_TRUE( anchorDiv != nullptr );
+ ASSERT_TRUE( anchorH2 != nullptr );
+ ASSERT_TRUE( anchorSpan != nullptr );
+
+ sceneNode->update( Time::Zero );
+
+ EXPECT_GT( anchor->getSize().getWidth(), 0 );
+ EXPECT_GT( anchorDiv->getSize().getWidth(), 0 );
+ EXPECT_GT( anchorH2->getSize().getWidth(), 0 );
+ EXPECT_GT( anchorSpan->getSize().getWidth(), 0 );
+ EXPECT_LE( anchor->getSize().getWidth(), parent->getSize().getWidth() );
+
+ eeDelete( sceneNode );
+ Engine::destroySingleton();
+}
+
+UTEST( UIRichText, InvalidWidthLengthComputation3 ) {
+ Engine::instance()->createWindow( WindowSettings( 800, 600, "Invalid Anchor Width 3",
+ WindowStyle::Default, WindowBackend::Default,
+ 32, {}, 1, false, true ) );
+ FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
+
+ FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
+ font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
+ ASSERT_TRUE( font->loaded() );
+ FontFamily::loadFromRegular( font );
+
+ UISceneNode* sceneNode = UISceneNode::New();
+ UIThemeManager* themeManager = sceneNode->getUIThemeManager();
+ themeManager->setDefaultFont( font );
+
+ String xml = R"xml(
+
+ )xml";
+
+ std::string html;
+ FileSystem::fileGet( "assets/html/blog_main_incorrect_widths.html", html );
+
+ sceneNode->loadLayoutFromString( xml );
+ auto htmlView = sceneNode->find( "html_view" );
+ ASSERT_TRUE( htmlView != nullptr );
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), htmlView );
+
+ auto container = sceneNode->getRoot()->querySelector( ".container" );
+ auto posts = sceneNode->getRoot()->querySelectorAll( ".post-list > a" );
+ auto items = sceneNode->getRoot()->querySelectorAll( ".post-list > a > .post-item-content" );
+ auto titles = sceneNode->getRoot()->querySelectorAll(
+ ".post-list > a > .post-item-content > .post-header-row" );
+
+ ASSERT_TRUE( container != nullptr );
+ ASSERT_TRUE( posts.size() > 0 );
+ ASSERT_TRUE( items.size() == posts.size() );
+ ASSERT_TRUE( items.size() == titles.size() );
+
+ sceneNode->update( Time::Zero );
+
+ for ( size_t i = 0; i < posts.size(); i++ ) {
+ auto anchor = posts[i];
+ auto item = items[i];
+ auto title = titles[i];
+ EXPECT_LE( anchor->getPixelsSize().getWidth(), container->getPixelsSize().getWidth() );
+ EXPECT_LE( item->getPixelsSize().getWidth(), anchor->getPixelsSize().getWidth() );
+ EXPECT_LE( title->getPixelsSize().getWidth(), item->getPixelsSize().getWidth() );
+ }
+
+ eeDelete( sceneNode );
+ Engine::destroySingleton();
+}