mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
Improved RichText custom sizes to indicate if it's a block.
This commit is contained in:
376
bin/unit_tests/assets/html/blog_main_incorrect_widths.html
Normal file
376
bin/unit_tests/assets/html/blog_main_incorrect_widths.html
Normal 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: ➜ 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 That’s 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>
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user