From bbd91d2399e1ae7e2fbe249f5f079df09e2d7012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 27 Apr 2026 23:58:13 -0300 Subject: [PATCH] Added support for margin and padding in text spans. --- .../assets/html/anchor_margins.html | 65 +++++++++ .../assets/html/eepp-ui-anchor-margins.webp | Bin 0 -> 1586 bytes .../assets/html/eepp-ui-span-padding.webp | Bin 0 -> 4764 bytes bin/unit_tests/assets/html/span_padding.html | 56 ++++++++ include/eepp/graphics/richtext.hpp | 11 +- src/eepp/graphics/richtext.cpp | 109 +++++++++++---- src/eepp/ui/uirichtext.cpp | 4 +- src/tests/unit_tests/richtext.cpp | 24 ++-- src/tests/unit_tests/uihtml_tests.cpp | 125 ++++++++++++++---- 9 files changed, 329 insertions(+), 65 deletions(-) create mode 100644 bin/unit_tests/assets/html/anchor_margins.html create mode 100644 bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp create mode 100644 bin/unit_tests/assets/html/eepp-ui-span-padding.webp create mode 100644 bin/unit_tests/assets/html/span_padding.html diff --git a/bin/unit_tests/assets/html/anchor_margins.html b/bin/unit_tests/assets/html/anchor_margins.html new file mode 100644 index 000000000..28183ac5a --- /dev/null +++ b/bin/unit_tests/assets/html/anchor_margins.html @@ -0,0 +1,65 @@ + + + + + + + + + diff --git a/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp b/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp new file mode 100644 index 0000000000000000000000000000000000000000..70f198101918eb6a2a6dde1a281648026607cb89 GIT binary patch literal 1586 zcmd6k>pK$)0LF(YmmG2_OL7h2xRV;@GP5n5A=6r^R+zCuRBq)G<~D3eJZ5XAt8|%G zD!Ig(ax0f;qleIrLkKg59i6}8JkR^#{rJAWUmyYoGv5XPgm`A9Pc(+duJByiO(Bcc`eysu3RXsbNYXRh zx09ghhGq<4VYdWl1owcNY3^50LwG2uDZTS;EtpEPJM6Jnm1JWS=MXb1XOD#CG>FoM zrqq;gvt4bf6%X%-R{f-aqR8gunrR0L<_-Wgy?W1RdtWbS6nlEP17!eAWgx8}t$a4X z-_Fcl{(!oz20BlFlb{>XX}e$6ftB_}>J!k*QK8P~jJV2D4RNw?YJ+d^Pu)b^F1KdX z5N9@;H9La1TZtn!+DGv@#NQF=BfbTLhaq1Q>30*sRl^%^Nt@s}K8(^oa&a$DG_xqj zCF&fSZi*qUojeD%iRL@0oAxd@X`$v~*XJM;hUaPuv13TmsfU^x0}6VRlN@@CZ=Zmg zdw;TZDB{<5<_xq}+jrn&xYoYEPZ)qw-mn0R$?eACrSxYG`u>y=*;L+ks$5;?qPetQ<8;r zA97+4+#4aFt%fO=9G~Z|z4`1)q`CnZuZnJ7eIm*^dM3Ea%jE=CeAN=#F&%G@Vh4ii zU7~i8QK=zZ^3Z{H{NT$($gK1$RNQ0@bV~^O{)}JThEHfod40_DMTmg@NApOnr(Ml+ z=L@d%XbK2cn(H@OF7K{h96-2LIXqPuicuW1Y_6Pe=}g*DdFtnIeTc^NAai%POGwp; z8+ys>`+^@NK9k#%5Lfi0<>@8MZY86nNOlFRG$M8vZqSGnSfk}=QQDHV4703Vt1Hm0 zwv_SP_O@o@v49-#mz8_Ua!hwyt}(Y=cXU2daU`_bADH$UlXVUWW_OV)Z^mbPgyE={ z!)1zr=WgsVBu7?opvD6V+M6zZ09ue+yuUu>gIAGiMXUXx7u~r#oK-y9dU82=^t%E^4$Dr~w2EMf?m&IgwZ1FLR$h6uudySVGO~Y#6x_Ujq z#NRlURj+6R^?dP#+yrPIsEx$4Q04unJGzQLw3%E`4mw0P$dEwOf*tkrIW<4Nh*QPW4lKYM6B41PO0D_D67_DPL0N~M%O6^b*X&VmJbyymCG|s7}foj ziY7X;@94jv)CPb90=u-rtF$s-g`~foHp@-tVUYGfu}3 zA>3j2&t!P<8%y#(UWth;FX4MHe!uj!Vn6oB`C>ff!@<^$t6VEZ1JHdQ!1~UcL2+m= z-v%AJeIQ{_yX2uxf7>fF3@C<3+evt^WuBIIyz?en$Bo0>I6K^zz`M3dE;>>P$DJ6( zT_?kZqC5mWJ1I_yS9`uC>k&-+WFj0MhEtuFY%~yB2(oraKdP{V@)+9)!l!m@K7&#@ zkC1O@>!2b0H!kjKmg| zL3L~7_vZtdn(RfLGVznwj(w1A&Y5W;+mGGTAU1UEm$b9gg(4;2WWyt5D&}P0THtuQ z;eYObUi|zX@%*%-3>ZmVf~-zUgqUNt8vR;*OYBwE^zOCM$yjLpe bdBLM66;*pQzF%mP&g)4R{_iny00904;zKW} literal 0 HcmV?d00001 diff --git a/bin/unit_tests/assets/html/eepp-ui-span-padding.webp b/bin/unit_tests/assets/html/eepp-ui-span-padding.webp new file mode 100644 index 0000000000000000000000000000000000000000..6ddf9b8fb73b409757fb9236e6e70cb929bebbef GIT binary patch literal 4764 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 literal 0 HcmV?d00001 diff --git a/bin/unit_tests/assets/html/span_padding.html b/bin/unit_tests/assets/html/span_padding.html new file mode 100644 index 000000000..5835d113f --- /dev/null +++ b/bin/unit_tests/assets/html/span_padding.html @@ -0,0 +1,56 @@ + + + + + + +
+ This is normal text with a padded span inside it. +
+
+ Here is a large padded span which should increase the line height and spacing. +
+
+ Finally, a mixed padding span to test asymmetrical padding values. +
+ + \ No newline at end of file diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp index d803b5f63..1c7a90ced 100644 --- a/include/eepp/graphics/richtext.hpp +++ b/include/eepp/graphics/richtext.hpp @@ -33,6 +33,9 @@ class EE_API RichText : public Drawable { */ void addSpan( const String& text, const FontStyleConfig& style ); + void addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin, + const Rectf& padding ); + /** * @brief Adds a text span with individual style parameters. * @param text The text content. @@ -82,7 +85,13 @@ class EE_API RichText : public Drawable { bool isBlock{ false }; }; - using Block = std::variant, std::shared_ptr, CustomBlock>; + struct SpanBlock { + std::shared_ptr text; + Rectf margin; + Rectf padding; + }; + + using Block = std::variant, CustomBlock>; /** * @brief Adds a drawable (e.g., an image) into the text flow. diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index 8bfc3705c..5e35eaace 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -57,7 +57,27 @@ void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, cons std::visit( Overloaded{ - [&]( const std::shared_ptr& text ) { + [&]( const SpanBlock& spanBlock ) { + const std::shared_ptr& text = spanBlock.text; + Color oldBgColor = text->getFontStyleConfig().BackgroundColor; + + if ( oldBgColor != Color::Transparent ) { + Primitives p; + p.setColor( oldBgColor ); + Rectf bgRect( + Vector2f( + std::trunc( X + pos.x - spanBlock.padding.Left ), + std::trunc( Y + line.y + pos.y - spanBlock.padding.Top ) ), + Sizef( span.size.getWidth() + spanBlock.padding.Left + + spanBlock.padding.Right, + span.size.getHeight() + spanBlock.padding.Top + + spanBlock.padding.Bottom ) ); + p.drawRectangle( bgRect, rotation, scale ); + } + + if ( oldBgColor != Color::Transparent ) + text->setBackgroundColor( Color::Transparent ); + bool selectionApplied = false; if ( mSelectionColor != Color::Transparent ) { TextSelectionRange spanSel = { @@ -85,6 +105,9 @@ void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, cons rotation, effect, rotationCenter, scaleCenter ); } + if ( oldBgColor != Color::Transparent ) + text->setBackgroundColor( oldBgColor ); + if ( selectionApplied ) text->invalidateColors(); }, @@ -127,9 +150,9 @@ Int64 RichText::findCharacterFromPos( const Vector2i& pos ) const { if ( pos.y >= line.y && pos.y < line.y + line.height ) { for ( const auto& span : line.spans ) { if ( pos.x >= span.position.x && pos.x < span.position.x + span.size.getWidth() ) { - if ( auto pText = std::get_if>( &span.block ) ) { + if ( auto pText = std::get_if( &span.block ) ) { return span.startCharIndex + - ( *pText )->findCharacterFromPos( Vector2i( + pText->text->findCharacterFromPos( Vector2i( pos.x - span.position.x, pos.y - line.y - span.position.y ) ); } else { return ( pos.x < span.position.x + span.size.getWidth() * 0.5f ) @@ -166,8 +189,8 @@ Vector2f RichText::findCharacterPos( Int64 index ) const { for ( const auto& line : mLines ) { for ( const auto& span : line.spans ) { if ( index >= span.startCharIndex && index < span.endCharIndex ) { - if ( auto pText = std::get_if>( &span.block ) ) { - Vector2f p = ( *pText )->findCharacterPos( index - span.startCharIndex ); + if ( auto pText = std::get_if( &span.block ) ) { + Vector2f p = pText->text->findCharacterPos( index - span.startCharIndex ); return { span.position.x + p.x, line.y + span.position.y + p.y }; } else { return { span.position.x, line.y + span.position.y }; @@ -197,8 +220,8 @@ SmallVector RichText::getSelectionRects() const { Int64 spanEnd = std::min( end, span.endCharIndex ); if ( spanStart < spanEnd ) { - if ( auto pText = std::get_if>( &span.block ) ) { - auto spanRects = ( *pText )->getSelectionRects( + if ( auto pText = std::get_if( &span.block ) ) { + auto spanRects = pText->text->getSelectionRects( { spanStart - span.startCharIndex, spanEnd - span.startCharIndex } ); for ( auto& rect : spanRects ) { rect.move( { span.position.x, line.y + span.position.y } ); @@ -237,9 +260,9 @@ String RichText::getSelectionString() const { Int64 spanEnd = std::min( end, span.endCharIndex ); if ( spanStart < spanEnd ) { - if ( auto pText = std::get_if>( &span.block ) ) { - res += ( *pText )->getString().substr( spanStart - span.startCharIndex, - spanEnd - spanStart ); + if ( auto pText = std::get_if( &span.block ) ) { + res += pText->text->getString().substr( spanStart - span.startCharIndex, + spanEnd - spanStart ); } else { // It's a drawable or custom size, it takes 1 "character" index. res += ' '; @@ -264,14 +287,15 @@ Sizef RichText::getPixelsSize() { return getSize(); } -void RichText::addSpan( const String& text, const FontStyleConfig& style ) { +void RichText::addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin, + const Rectf& padding ) { if ( text.empty() ) return; auto span = std::make_shared(); span->setString( text ); span->setStyleConfig( style ); - mBlocks.push_back( span ); // Implicitly constructs the variant's Text alternative + mBlocks.push_back( SpanBlock{ span, margin, padding } ); invalidateLayout(); } @@ -287,6 +311,10 @@ void RichText::addCustomSize( const Sizef& size, bool isBlock ) { invalidateLayout(); } +void RichText::addSpan( const String& text, const FontStyleConfig& style ) { + addSpan( text, style, Rectf(), Rectf() ); +} + void RichText::addSpan( const String& text, Font* font, Uint32 characterSize, Color color, Uint32 style, Color backgroundColor ) { FontStyleConfig config; @@ -332,9 +360,9 @@ void RichText::setMaxWidth( Float width ) { void RichText::invalidate() { invalidateLayout(); for ( auto& block : mBlocks ) { - if ( auto pText = std::get_if>( &block ) ) { - if ( *pText ) - ( *pText )->invalidate(); + if ( auto pText = std::get_if( &block ) ) { + if ( pText->text ) + pText->text->invalidate(); } } } @@ -342,8 +370,8 @@ void RichText::invalidate() { Float RichText::getMinIntrinsicWidth() { Float minW = 0; for ( auto& block : mBlocks ) { - if ( auto pText = std::get_if>( &block ) ) { - auto& span = *pText; + if ( auto pText = std::get_if( &block ) ) { + auto& span = pText->text; if ( !span || span->getString().empty() ) continue; const String& s = span->getString(); @@ -359,7 +387,9 @@ Float RichText::getMinIntrinsicWidth() { end++; if ( start < end ) { minW = std::max( minW, Text::getTextWidth( s.substr( start, end - start ), - span->getFontStyleConfig() ) ); + span->getFontStyleConfig() ) + + pText->margin.Left + pText->margin.Right + + pText->padding.Left + pText->padding.Right ); } start = end; } @@ -376,23 +406,25 @@ Float RichText::getMaxIntrinsicWidth() { Float maxW = 0; Float curX = 0; for ( auto& block : mBlocks ) { - if ( auto pText = std::get_if>( &block ) ) { - auto& span = *pText; + if ( auto pText = std::get_if( &block ) ) { + auto& span = pText->text; if ( !span || span->getString().empty() ) continue; const String& s = span->getString(); size_t start = 0; size_t end = 0; + curX += pText->margin.Left + pText->padding.Left; while ( ( end = s.find( '\n', start ) ) != String::InvalidPos ) { curX += Text::getTextWidth( s.substr( start, end - start ), span->getFontStyleConfig(), 4, span->getTextHints() ); - maxW = std::max( maxW, curX ); + maxW = std::max( maxW, curX + pText->margin.Right + pText->padding.Right ); curX = 0; start = end + 1; } curX += Text::getTextWidth( s.substr( start ), span->getFontStyleConfig(), 4, - span->getTextHints() ); + span->getTextHints() ) + + pText->margin.Right + pText->padding.Right; } else if ( auto pDrawable = std::get_if>( &block ) ) { curX += ( *pDrawable )->getPixelsSize().getWidth(); } else if ( auto pSize = std::get_if( &block ) ) { @@ -423,8 +455,8 @@ void RichText::updateLayout() { Int64 curCharIdx = 0; for ( auto& block : mBlocks ) { - if ( auto pText = std::get_if>( &block ) ) { - auto& span = *pText; + if ( auto pText = std::get_if( &block ) ) { + auto& span = pText->text; if ( !span || span->getString().empty() ) continue; @@ -432,6 +464,11 @@ void RichText::updateLayout() { if ( !fontStyle.Font ) continue; + Float extraLeft = pText->margin.Left + pText->padding.Left; + curX += extraLeft; + if ( !mLines.empty() ) + mLines.back().width += extraLeft; + Uint32 textHints = span->getTextHints(); LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx( @@ -460,7 +497,7 @@ void RichText::updateLayout() { Float spanWidth = renderSpanText->getTextWidth(); RenderSpan renderSpan; - renderSpan.block = renderSpanText; + renderSpan.block = SpanBlock{ renderSpanText, pText->margin, pText->padding }; renderSpan.position = { curX, 0 }; // Y adjusted later renderSpan.size = Sizef( spanWidth, height ); // Configured BEFORE pushing to vector @@ -478,12 +515,30 @@ void RichText::updateLayout() { currentLine.width += spanWidth; } + if ( i == wrapInfo.wraps.size() - 2 && !isNewline ) { + Float extraRight = pText->margin.Right + pText->padding.Right; + curX += extraRight; + mLines.back().width += extraRight; + if ( !isNewline && mMaxWidth > 0 && curX > mMaxWidth ) { + // the margin forced a wrap + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + continue; // skip the next newline check + } + } + // If it's a newline, or if it's not the very last segment (which means it wrapped), // start a new line. Exception: If the last segment was just a newline, we already // handled it. if ( i < wrapInfo.wraps.size() - 2 || isNewline ) { if ( isNewline ) { curCharIdx++; + if ( i == wrapInfo.wraps.size() - 2 ) { + Float extraRight = pText->margin.Right + pText->padding.Right; + curX += extraRight; + mLines.back().width += extraRight; + } } maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); @@ -562,8 +617,8 @@ void RichText::updateLayout() { Float maxLineHeight = 0; for ( auto& span : line.spans ) { - if ( auto pText = std::get_if>( &span.block ) ) { - auto& textBlock = *pText; + if ( auto pText = std::get_if( &span.block ) ) { + auto& textBlock = pText->text; Float offsetY = line.maxAscent - textBlock->getCharacterSize(); span.position.x += xOffset; span.position.y = offsetY; diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index adb71dba3..e8cbe10f1 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -592,7 +592,9 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( widget->isType( UI_TYPE_TEXTSPAN ) ) { UITextSpan* span = widget->asType(); if ( !span->getText().empty() ) { - richText.addSpan( span->getText(), span->getFontStyleConfig() ); + Rectf margin = span->getLayoutPixelsMargin(); + Rectf padding = span->getPixelsPadding(); + richText.addSpan( span->getText(), span->getFontStyleConfig(), margin, padding ); } Node* spanChild = span->getFirstChild(); while ( spanChild != NULL ) { diff --git a/src/tests/unit_tests/richtext.cpp b/src/tests/unit_tests/richtext.cpp index 2aec19b61..56cba3e2d 100644 --- a/src/tests/unit_tests/richtext.cpp +++ b/src/tests/unit_tests/richtext.cpp @@ -362,8 +362,8 @@ UTEST( UIRichText, IntegrationAndLayoutVerification ) { ASSERT_EQ( blocks.size(), (size_t)4 ); // Check Text block - EXPECT_TRUE( std::holds_alternative>( blocks[1] ) ); - auto text1 = std::get>( blocks[1] ); + EXPECT_TRUE( std::holds_alternative( blocks[1] ) ); + auto text1 = std::get( blocks[1] ).text; EXPECT_TRUE( text1->getFillColor() == Color::fromString( "#FF0000" ) ); // Check CustomSize block @@ -374,7 +374,7 @@ UTEST( UIRichText, IntegrationAndLayoutVerification ) { UI::UIWidget* placeholder = rt->find( "placeholder" ); ASSERT_TRUE( placeholder != nullptr ); - auto text0 = std::get>( blocks[0] ); + auto text0 = std::get( blocks[0] ).text; Vector2f pos = placeholder->getPixelsPosition(); Float expectedX = text0->getTextWidth() + text1->getTextWidth(); EXPECT_NEAR( pos.x, expectedX, 2.0f ); @@ -464,10 +464,10 @@ UTEST( UIRichText, NestedWidgetsIntegration ) { ASSERT_EQ( blocks.size(), (size_t)4 ); // Check block types - EXPECT_TRUE( std::holds_alternative>( blocks[0] ) ); - EXPECT_TRUE( std::holds_alternative>( blocks[1] ) ); + 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[3] ) ); + EXPECT_TRUE( std::holds_alternative( blocks[3] ) ); EXPECT_EQ( std::get( blocks[2] ).size.getWidth(), PixelDensity::dpToPx( 50 ) ); @@ -478,8 +478,8 @@ UTEST( UIRichText, NestedWidgetsIntegration ) { UI::UIWidget* placeholder = rt->find( "placeholder" ); ASSERT_TRUE( placeholder != nullptr ); - auto text0 = std::get>( blocks[0] ); - auto text1 = std::get>( blocks[1] ); + auto text0 = std::get( blocks[0] ).text; + auto text1 = std::get( blocks[1] ).text; Vector2f pos = placeholder->getScreenPos(); Float expectedX = text0->getTextWidth() + text1->getTextWidth(); @@ -528,14 +528,14 @@ UTEST( UIRichText, DefaultStyleInheritance ) { // blocks[1] should be "Small" with overridden size and color ASSERT_TRUE( blocks.size() >= 2 ); - EXPECT_TRUE( std::holds_alternative>( blocks[0] ) ); - auto text0 = std::get>( blocks[0] ); + EXPECT_TRUE( std::holds_alternative( blocks[0] ) ); + auto text0 = std::get( blocks[0] ).text; EXPECT_EQ( text0->getCharacterSize(), rt->getFontSize() ); EXPECT_EQ( text0->getFillColor().getValue(), rt->getFontColor().getValue() ); EXPECT_EQ( text0->getFillColor().getValue(), Color::fromString( "#FF0000" ).getValue() ); - EXPECT_TRUE( std::holds_alternative>( blocks[1] ) ); - auto text1 = std::get>( blocks[1] ); + EXPECT_TRUE( std::holds_alternative( blocks[1] ) ); + auto text1 = std::get( blocks[1] ).text; EXPECT_EQ( text1->getCharacterSize(), (unsigned int)PixelDensity::dpToPxI( 16 ) ); EXPECT_EQ( text1->getFillColor().getValue(), Color::fromString( "#00FF00" ).getValue() ); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 997d0629b..568efd85e 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -140,6 +140,74 @@ UTEST( UIHTMLTable, complexLayout2 ) { Engine::destroySingleton(); } +UTEST( UIRichText, anchorMargins ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 800, 600, "Anchor Margins Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + ASSERT_TRUE( font != nullptr && font->loaded() ); + FontFamily::loadFromRegular( font ); + + UI::UISceneNode* sceneNode = UI::UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); + themeManager->setDefaultFont( font ); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + std::string html; + FileSystem::fileGet( "assets/html/anchor_margins.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + win->setClearColor( Color::White ); + + win->getInput()->update(); + SceneManager::instance()->update(); + + win->clear(); + SceneManager::instance()->draw(); + win->display(); + + compareImages( utest_state, utest_result, win, "eepp-ui-anchor-margins", "html" ); + + Engine::destroySingleton(); +} + +UTEST( UIRichText, spanPadding ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 800, 600, "Span Padding Test", WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + ASSERT_TRUE( font != nullptr && font->loaded() ); + FontFamily::loadFromRegular( font ); + + UI::UISceneNode* sceneNode = UI::UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); + themeManager->setDefaultFont( font ); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + std::string html; + FileSystem::fileGet( "assets/html/span_padding.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + win->setClearColor( Color::White ); + + win->getInput()->update(); + SceneManager::instance()->update(); + + win->clear(); + SceneManager::instance()->draw(); + win->display(); + + compareImages( utest_state, utest_result, win, "eepp-ui-span-padding", "html" ); + + Engine::destroySingleton(); +} + UTEST( UIHTMLTable, complexLayout3 ) { auto win = Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test 3", WindowStyle::Default, @@ -587,8 +655,7 @@ UTEST( UILayout, listStyleTypeDecimal ) { sceneNode->updateDirtyLayouts(); - const auto* propDef = - StyleSheetSpecification::instance()->getProperty( "list-style-type" ); + const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); ASSERT_TRUE( propDef != nullptr ); auto* li1 = sceneNode->getRoot()->find( "li1" )->asType(); @@ -619,8 +686,7 @@ UTEST( UILayout, listStyleTypeDisc ) { sceneNode->updateDirtyLayouts(); - const auto* propDef = - StyleSheetSpecification::instance()->getProperty( "list-style-type" ); + const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); ASSERT_TRUE( propDef != nullptr ); auto* li1 = sceneNode->getRoot()->find( "li1" )->asType(); @@ -651,10 +717,8 @@ UTEST( UILayout, listStyleShorthand ) { sceneNode->updateDirtyLayouts(); - const auto* typeDef = - StyleSheetSpecification::instance()->getProperty( "list-style-type" ); - const auto* posDef = - StyleSheetSpecification::instance()->getProperty( "list-style-position" ); + const auto* typeDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); + const auto* posDef = StyleSheetSpecification::instance()->getProperty( "list-style-position" ); for ( const char* id : { "li1", "li2", "li3", "li4", "li5", "li6" } ) { auto* li = sceneNode->getRoot()->find( id )->asType(); @@ -662,18 +726,27 @@ UTEST( UILayout, listStyleShorthand ) { EXPECT_TRUE( li->isType( UI_TYPE_HTML_LIST_ITEM ) ); } - EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType()->getPropertyString( typeDef ) == "decimal" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType()->getPropertyString( posDef ) == "outside" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType()->getPropertyString( + typeDef ) == "decimal" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType()->getPropertyString( + posDef ) == "outside" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType()->getPropertyString( typeDef ) == "lower-alpha" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType()->getPropertyString( posDef ) == "inside" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType()->getPropertyString( + typeDef ) == "lower-alpha" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType()->getPropertyString( + posDef ) == "inside" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li3" )->asType()->getPropertyString( typeDef ) == "none" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li3" )->asType()->getPropertyString( + typeDef ) == "none" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li4" )->asType()->getPropertyString( typeDef ) == "disc" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType()->getPropertyString( typeDef ) == "square" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType()->getPropertyString( posDef ) == "outside" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "li6" )->asType()->getPropertyString( typeDef ) == "circle" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li4" )->asType()->getPropertyString( + typeDef ) == "disc" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType()->getPropertyString( + typeDef ) == "square" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType()->getPropertyString( + posDef ) == "outside" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "li6" )->asType()->getPropertyString( + typeDef ) == "circle" ); Engine::destroySingleton(); } @@ -714,14 +787,18 @@ UTEST( UILayout, listStyleInheritanceFromUl ) { sceneNode->updateDirtyLayouts(); - const auto* typeDef = - StyleSheetSpecification::instance()->getProperty( "list-style-type" ); + const auto* typeDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "h1" )->asType()->getPropertyString( typeDef ) == "upper-roman" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "a1" )->asType()->getPropertyString( typeDef ) == "circle" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "b1" )->asType()->getPropertyString( typeDef ) == "disc" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "c1" )->asType()->getPropertyString( typeDef ) == "square" ); - EXPECT_TRUE( sceneNode->getRoot()->find( "d1" )->asType()->getPropertyString( typeDef ) == "decimal" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "h1" )->asType()->getPropertyString( + typeDef ) == "upper-roman" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "a1" )->asType()->getPropertyString( + typeDef ) == "circle" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "b1" )->asType()->getPropertyString( + typeDef ) == "disc" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "c1" )->asType()->getPropertyString( + typeDef ) == "square" ); + EXPECT_TRUE( sceneNode->getRoot()->find( "d1" )->asType()->getPropertyString( + typeDef ) == "decimal" ); Engine::destroySingleton(); }