Improved RichText custom sizes to indicate if it's a block.

This commit is contained in:
Martín Lucas Golini
2026-04-08 00:28:23 -03:00
parent 20c826d2ae
commit 311dffba7c
5 changed files with 618 additions and 40 deletions

View File

@@ -0,0 +1,376 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
:root {
--bg-color: rgb(11, 26, 10);
--text-primary: #e5e5e5;
--text-secondary: #d4d4d4;
--text-muted: #a3a3a3;
--text-dim: #737373;
--accent: #fbbf24;
--border-color: #171717;
--white: #ffffff;
--font-main: 'Fira Code', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-color);
color: var(--text-primary);
font-family: var(--font-main);
-webkit-font-smoothing: antialiased;
min-height: 100vh;
line-height: 1.5;
}
.container {
max-width: 48rem;
margin: 0 auto;
padding: 6rem 1.5rem;
}
@media (min-width: 768px) {
.container { padding-left: 3rem; padding-right: 3rem; }
}
a { color: inherit; text-decoration: none; transition: color 0.2s; }
/* Header & Nav */
.site-header { margin-bottom: 3rem; }
.site-header-index { margin-bottom: 4rem; }
.nav-bar { display: block; }
.site-brand {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
white-space: nowrap;
font-size: 1rem;
}
@media (min-width: 768px) { .site-brand { font-size: 1.125rem; } }
.brand-sid {
color: var(--accent);
border: 3px solid var(--accent);
border-right: none;
padding: 5px 10px 5px 20px;
text-decoration: none;
transition: all 0.2s;
line-height: 1;
}
.brand-sid:hover {
color: var(--white);
border-color: var(--white);
}
.brand-blog {
color: var(--accent);
border: 3px solid var(--accent);
border-left: none;
padding: 5px 20px 5px 10px;
text-decoration: none;
transition: all 0.2s;
margin-left: 0;
line-height: 1;
}
.brand-blog:hover {
color: var(--white);
border-color: var(--white);
}
.header-stripe {
content: "";
background: repeating-linear-gradient(90deg,var(--accent),var(--accent) 10px,transparent 0,transparent 20px);
display: block;
height: 40px;
opacity: 0.7;
}
.page-title {
font-size: 1.875rem;
font-weight: 700;
letter-spacing: -0.025em;
color: var(--white);
margin-bottom: 0.5rem;
margin-top: 0.5rem;
text-align: center;
line-height: 1.25;
}
@media (min-width: 768px) { .page-title { font-size: 3rem; } }
.page-title-index {
font-size: 1.875rem;
font-weight: 700;
letter-spacing: -0.025em;
color: var(--white);
margin-bottom: 1rem;
margin-top: 0.5rem;
text-align: center;
}
@media (min-width: 768px) { .page-title-index { font-size: 3rem; } }
.post-meta {
font-size: 0.875rem;
color: var(--text-dim);
border-top: 1px solid var(--border-color);
padding-top: 0.5rem;
margin-top: 0.5rem;
text-align: center;
}
/* Post Content */
.prose { color: var(--text-secondary); max-width: none; }
.prose h1 { font-size: 2.25rem; font-weight: 700; margin-bottom: 1.5rem; color: var(--white); margin-top: 0; line-height: 1.2; }
.prose h2 { font-size: 1.5rem; font-weight: 700; margin-top: 2rem; margin-bottom: 1rem; color: var(--white); line-height: 1.3; border-bottom: 4px solid #a98424; display: inline-block; margin-bottom: 30px;}
.prose h3 { font-size: 1.25rem; font-weight: 700; margin-top: 1.5rem; margin-bottom: 0.75rem; color: var(--white); }
.prose p { margin-bottom: 1.25rem; line-height: 1.75; }
.prose hr { margin: 2rem 0; border: 1px solid transparent; border-image: repeating-linear-gradient(to right, #b1b1b1 0 25px, transparent 0px 40px) 1; }
.prose a { color: var(--accent); border-bottom: 2px solid rgba(251, 191, 36, 0.7); transition: all 0.2s; }
.prose a:hover { border-bottom-color: var(--accent); color: var(--accent); }
.prose ul { list-style-type: &#10140; padding-left: 1.5rem; margin-bottom: 1.25rem; }
.prose li { margin-bottom: 0.5rem; }
.prose code { background: #000; color: #82d87c; padding: 0.2em 0.6em; border-radius: 0.25rem; font-size: 0.875em; font-family: var(--font-main); }
.prose pre { background: #1f1f1f; padding: 1rem; overflow-x: auto; border-radius: 0.5rem; margin-bottom: 1.5rem; }
.prose pre code { background: transparent; padding: 0; }
.prose-intro { margin-bottom: 5rem; color: var(--text-secondary); text-align: center; }
/* Post List */
.post-list { display: block; }
.post-item {
display: block;
position: relative;
text-decoration: none;
}
.post-item-bg {
background-color: transparent;
border-radius: 0.75rem;
transition: background-color 0.3s;
z-index: -1;
}
.post-item:hover .post-item-bg { background-color: rgba(23, 23, 23, 0.5); }
.post-item-content {
position: relative;
display: block;
padding: 0.5rem;
}
.post-item-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
transition: color 0.2s;
margin: 0;
}
@media (min-width: 768px) { .post-item-title { font-size: 1.5rem; } }
.post-item:hover .post-item-title { color: var(--accent); }
.post-item-date {
font-size: 0.75rem;
color: var(--text-dim);
margin-left: 0;
}
@media (min-width: 768px) { .post-item-date { margin-left: 1rem; } }
.read-more {
font-size: 0.875rem;
font-weight: 400;
color: var(--text-muted);
opacity: 0.6;
transition: opacity 0.2s;
}
.post-item:hover .read-more { opacity: 1; }
.arrow-icon { transition: transform 0.2s; display: inline-block; color: var(--accent); }
.post-item:hover .arrow-icon { transform: translateX(0.25rem); }
.divider { height: 1px; background-color: var(--border-color); width: 100%; margin: 0; border: none; display: block; }
.divider:last-child { display: none; }
.empty-state {
color: var(--text-dim);
font-style: italic;
padding: 1rem;
border: 1px dashed #262626;
border-radius: 0.25rem;
}
/* Footer */
.site-footer {
margin-top: 5rem;
padding-top: 2.5rem;
border-top: 1px solid var(--border-color);
font-size: 0.875rem;
color: var(--accent);
display: block;
}
.site-footer a { display: block; transition: color 0.2s; }
.site-footer a:hover { color: var(--white); }
</style>
</head>
<body>
<div class="container">
<header class="site-header-index">
<div class="nav-bar">
<div class="header-stripe"></div>
<p class="site-brand">
<a href="/" class="brand-sid">Sid's</a>
<a href="/blog/" class="brand-blog">Blog</a>
</p>
<div class="header-stripe"></div>
</div>
</header>
<div class="prose prose-intro">
<p>Personal pieces of rambling reflections and rants, tangential thoughts and tirades.</p>
</div>
<div class="post-list">
<a href="/blog/wont-download-your-app" class="post-item group">
<div class="post-item-bg"></div>
<div class="post-item-content">
<div class="post-header-row">
<h2 class="post-item-title">
No, I Won't Download Your App. The Web Version is A-OK.
</h2>
<span class="post-item-date">Apr 6, 2026</span>
</div>
<div class="read-more">
<span>Read post</span>
<span class="arrow-icon"></span>
</div>
</div>
</a>
<hr class="divider" />
<a href="/blog/667mhz-machine" class="post-item group">
<div class="post-item-bg"></div>
<div class="post-item-content">
<div class="post-header-row">
<h2 class="post-item-title">
The 667MHz Machine
</h2>
<span class="post-item-date">Mar 25, 2026</span>
</div>
<div class="read-more">
<span>Read post</span>
<span class="arrow-icon"></span>
</div>
</div>
</a>
<hr class="divider" />
<a href="/blog/online-tld-is-pain" class="post-item group">
<div class="post-item-bg"></div>
<div class="post-item-content">
<div class="post-header-row">
<h2 class="post-item-title">
Never Buy A .online Domain
</h2>
<span class="post-item-date">Feb 25, 2026</span>
</div>
<div class="read-more">
<span>Read post</span>
<span class="arrow-icon"></span>
</div>
</div>
</a>
<hr class="divider" />
<a href="/blog/accelerated-ai-fomo" class="post-item group">
<div class="post-item-bg"></div>
<div class="post-item-content">
<div class="post-header-row">
<h2 class="post-item-title">
Accelerated FOMO in the Age of AI
</h2>
<span class="post-item-date">Feb 23, 2026</span>
</div>
<div class="read-more">
<span>Read post</span>
<span class="arrow-icon"></span>
</div>
</div>
</a>
<hr class="divider" />
<a href="/blog/aidr" class="post-item group">
<div class="post-item-bg"></div>
<div class="post-item-content">
<div class="post-header-row">
<h2 class="post-item-title">
ai;dr
</h2>
<span class="post-item-date">Feb 12, 2026</span>
</div>
<div class="read-more">
<span>Read post</span>
<span class="arrow-icon"></span>
</div>
</div>
</a>
<hr class="divider" />
<a href="/blog/apple-review-is-rng" class="post-item group">
<div class="post-item-bg"></div>
<div class="post-item-content">
<div class="post-header-row">
<h2 class="post-item-title">
App Store Review Feels Like RNG, and Thats the Problem
</h2>
<span class="post-item-date">Feb 5, 2026</span>
</div>
<div class="read-more">
<span>Read post</span>
<span class="arrow-icon"></span>
</div>
</div>
</a>
<hr class="divider" />
<a href="/blog/welcome" class="post-item group">
<div class="post-item-bg"></div>
<div class="post-item-content">
<div class="post-header-row">
<h2 class="post-item-title">
Welcome to the Machine
</h2>
<span class="post-item-date">Feb 3, 2026</span>
</div>
<div class="read-more">
<span>Read post</span>
<span class="arrow-icon"></span>
</div>
</div>
</a>
<hr class="divider" />
</div>
<footer class="site-footer">
<a href="/">← Return to System</a>
<a href="/blog/rss.xml" aria-label="RSS Feed">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg>
RSS Feed
</a>
</footer>
</div>
</body>
</html>

View File

@@ -77,7 +77,12 @@ class EE_API RichText : public Drawable {
enum class BlockType { Text, Drawable, CustomSize };
using Block = std::variant<std::shared_ptr<Text>, std::shared_ptr<Drawable>, Sizef>;
struct CustomBlock {
Sizef size;
bool isBlock{ false };
};
using Block = std::variant<std::shared_ptr<Text>, std::shared_ptr<Drawable>, 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<Block>& getBlocks() { return mBlocks; }

View File

@@ -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> 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<std::shared_ptr<Drawable>>( &block ) ) {
minW = std::max( minW, ( *pDrawable )->getPixelsSize().getWidth() );
} else if ( auto pSize = std::get_if<Sizef>( &block ) ) {
minW = std::max( minW, pSize->getWidth() );
} else if ( auto pSize = std::get_if<CustomBlock>( &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<std::shared_ptr<Drawable>>( &block ) ) {
curX += ( *pDrawable )->getPixelsSize().getWidth();
} else if ( auto pSize = std::get_if<Sizef>( &block ) ) {
curX += pSize->getWidth();
} else if ( auto pSize = std::get_if<CustomBlock>( &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<std::shared_ptr<Drawable>>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
} else if ( auto pSize = std::get_if<Sizef>( &block ) ) {
blockSize = *pSize;
} else if ( auto pSize = std::get_if<CustomBlock>( &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;

View File

@@ -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<Sizef>( span.block ) )
if ( std::holds_alternative<RichText::CustomBlock>( span.block ) )
return &span;
}
currentSpan = 0;

View File

@@ -9,6 +9,7 @@
#include <eepp/system/filesystem.hpp>
#include <eepp/system/scopedop.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uirichtext.hpp>
@@ -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<Sizef>( blocks[2] ) );
EXPECT_EQ( std::get<Sizef>( blocks[2] ).getWidth(), PixelDensity::dpToPx( 50 ) );
EXPECT_TRUE( std::holds_alternative<RichText::CustomBlock>( blocks[2] ) );
EXPECT_EQ( std::get<RichText::CustomBlock>( blocks[2] ).size.getWidth(),
PixelDensity::dpToPx( 50 ) );
UI::UIWidget* placeholder = rt->find<UI::UIWidget>( "placeholder" );
ASSERT_TRUE( placeholder != nullptr );
@@ -463,10 +466,11 @@ UTEST( UIRichText, NestedWidgetsIntegration ) {
// Check block types
EXPECT_TRUE( std::holds_alternative<std::shared_ptr<Graphics::Text>>( blocks[0] ) );
EXPECT_TRUE( std::holds_alternative<std::shared_ptr<Graphics::Text>>( blocks[1] ) );
EXPECT_TRUE( std::holds_alternative<Sizef>( blocks[2] ) );
EXPECT_TRUE( std::holds_alternative<RichText::CustomBlock>( blocks[2] ) );
EXPECT_TRUE( std::holds_alternative<std::shared_ptr<Graphics::Text>>( blocks[3] ) );
EXPECT_EQ( std::get<Sizef>( blocks[2] ).getWidth(), PixelDensity::dpToPx( 50 ) );
EXPECT_EQ( std::get<RichText::CustomBlock>( blocks[2] ).size.getWidth(),
PixelDensity::dpToPx( 50 ) );
UI::UIWidget* strongNode = rt->find<UI::UIWidget>( "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(
<ScrollView id="html_view" layout_width="match_parent" layout_height="match_parent" />
)xml";
String html = R"html(
<!doctype html>
<html lang="en">
<body>
<div>
<div id="anchor_parent">
<a id="anchor" href="#">
<div></div>
<div>
<div>
<h2>No, I Won't Download Your App. The Web Version is A-OK.</h2>
</div>
</div>
</a>
</div>
<footer class="site-footer">
<a href="/"> Return to System</a>
</footer>
</div>
</body>
</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<UIWidget>( "anchor_parent" );
auto anchor = sceneNode->find<UIWidget>( "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(
<ScrollView id="html_view" layout_width="match_parent" layout_height="match_parent" />
)xml";
String html = R"html(<!doctype html>
<html lang="en">
<body>
<div class="container">
<div id="anchor_parent">
<a id="anchor" href="#">
<div id="anchor_div">
<h2 id="anchor_h2">
No, I Won't Download Your App. The Web Version is A-OK.
</h2>
<span id="anchor_span">Apr 6, 2026</span>
</div>
</a>
</div>
</div>
</body>
</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<UIWidget>( "anchor_parent" );
auto anchor = sceneNode->find<UIWidget>( "anchor" );
auto anchorDiv = sceneNode->find<UIWidget>( "anchor_div" );
auto anchorH2 = sceneNode->find<UIWidget>( "anchor_h2" );
auto anchorSpan = sceneNode->find<UIWidget>( "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(
<ScrollView id="html_view" layout_width="match_parent" layout_height="match_parent" />
)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();
}