From efc361b654f8e5db3cebfed6fecf04c668a8fadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 25 Jan 2026 14:53:55 -0300 Subject: [PATCH] LineWraps now also report real new line breaks (otherwise it will not be possible to partition into visual lines). Added more text hard-wrap tests. RGB now has a constructor that accepts Color. Color now has `toRGB()`. --- .../fontrendering/eepp-text-hard-wrap.webp | Bin 0 -> 11108 bytes .../assets/textfiles/test-hard-wrap.uext | 14 ++ include/eepp/graphics/linewrap.hpp | 23 +-- include/eepp/graphics/text.hpp | 5 +- include/eepp/graphics/textshaperun.hpp | 2 + include/eepp/system/color.hpp | 6 + src/eepp/graphics/linewrap.cpp | 8 ++ src/eepp/graphics/text.cpp | 17 ++- src/eepp/graphics/textlayout.cpp | 31 ++-- src/eepp/graphics/textshaperun.cpp | 4 + src/eepp/system/color.cpp | 6 + src/tests/unit_tests/fontrendering.cpp | 89 ++++++++++++ src/tests/unit_tests/utest.hpp | 136 +++++++++++------- 13 files changed, 263 insertions(+), 78 deletions(-) create mode 100644 bin/unit_tests/assets/fontrendering/eepp-text-hard-wrap.webp create mode 100644 bin/unit_tests/assets/textfiles/test-hard-wrap.uext diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-hard-wrap.webp b/bin/unit_tests/assets/fontrendering/eepp-text-hard-wrap.webp new file mode 100644 index 0000000000000000000000000000000000000000..84550af56d8ce1add7cd11a19dbbd512056b6ff2 GIT binary patch literal 11108 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 tabOffset = {} ); + std::optional tabOffset = {}, Uint32 textHints = 0 ); static bool hardWrapText( String& string, const Float& maxWidth, const FontStyleConfig& config, - const Uint32& tabWidth = 4, std::optional tabOffset = {} ); + const Uint32& tabWidth = 4, std::optional tabOffset = {}, + Uint32 textHints = 0 ); static Text* New(); diff --git a/include/eepp/graphics/textshaperun.hpp b/include/eepp/graphics/textshaperun.hpp index 98ed57041..d9d1bdc0c 100644 --- a/include/eepp/graphics/textshaperun.hpp +++ b/include/eepp/graphics/textshaperun.hpp @@ -20,6 +20,8 @@ class EE_API TextShapeRun { std::size_t pos() const; + std::size_t length() const; + void next(); bool runIsNewLine() const; diff --git a/include/eepp/system/color.hpp b/include/eepp/system/color.hpp index 99247644b..a304923b9 100644 --- a/include/eepp/system/color.hpp +++ b/include/eepp/system/color.hpp @@ -148,6 +148,8 @@ template class tColor { typedef tColor ColorAf; typedef tColor Colorf; +class RGB; + class EE_API Color : public tColor { public: Color(); @@ -176,6 +178,8 @@ class EE_API Color : public tColor { Colorf toHsl() const; + RGB toRGB() const; + Color clone() const; Color invert() const; @@ -402,6 +406,8 @@ class EE_API RGB : public tRGB { RGB( const tRGB& color ); + RGB( const Color& color ); + RGB( Uint32 Col ); Color toColor(); diff --git a/src/eepp/graphics/linewrap.cpp b/src/eepp/graphics/linewrap.cpp index 79a3c6d31..b68488b2b 100644 --- a/src/eepp/graphics/linewrap.cpp +++ b/src/eepp/graphics/linewrap.cpp @@ -90,11 +90,18 @@ LineWrapInfo LineWrap::computeLineBreaks( const String::View& string, Font* font info.paddingStart = layout->paragraphs.front().wrapInfo.paddingStart; + std::size_t reserve = 0; + for ( auto& paragraph : layout->paragraphs ) + reserve += paragraph.wrapInfo.wraps.size(); + info.wraps.reserve( reserve ); + for ( auto& paragraph : layout->paragraphs ) { for ( const auto& wrap : paragraph.wrapInfo.wraps ) info.wraps.push_back( wrap ); } + std::sort( info.wraps.begin(), info.wraps.end() ); + return info; } #endif @@ -127,6 +134,7 @@ LineWrapInfo LineWrap::computeLineBreaks( const String::View& string, Font* font if ( curChar == '\n' ) { xoffset = 0; lastSpace = idx; + info.wraps.push_back( lastSpace ); idx++; continue; } diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index 6c8c89ec4..4b750c150 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -518,28 +518,31 @@ Sizef Text::draw( const StringType& string, const Vector2f& pos, const FontStyle bool Text::hardWrapText( Font* font, const Uint32& fontSize, String& string, const Float& maxWidth, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, - std::optional tabOffset ) { + std::optional tabOffset, Uint32 textHints ) { if ( string.empty() || NULL == font ) return false; LineWrapInfo lineWrap = LineWrap::computeLineBreaks( string, font, fontSize, maxWidth, LineWrapMode::Word, style, outlineThickness, - tabOffset.has_value(), tabWidth, tabOffset ? *tabOffset : 0.f ); + tabOffset.has_value(), tabWidth, tabOffset ? *tabOffset : 0.f, textHints ); if ( lineWrap.wraps.size() <= 1 ) return false; - for ( Int64 i = lineWrap.wraps.size() - 1; i > 0; i-- ) - string.insert( lineWrap.wraps[i], '\n' ); + for ( Int64 i = lineWrap.wraps.size() - 1; i > 0; i-- ) { + if ( string[lineWrap.wraps[i]] != '\n' ) // Skip real line-breaks! + string.insert( lineWrap.wraps[i], '\n' ); + } return true; } bool Text::hardWrapText( String& string, const Float& maxWidth, const FontStyleConfig& config, - const Uint32& tabWidth, std::optional tabOffset ) { + const Uint32& tabWidth, std::optional tabOffset, + Uint32 textHints ) { return hardWrapText( config.Font, config.CharacterSize, string, maxWidth, config.Style, - tabWidth, config.OutlineThickness, tabOffset ); + tabWidth, config.OutlineThickness, tabOffset, textHints ); } Text::Text() {} @@ -1309,7 +1312,7 @@ void Text::updateWidthCache() { } void Text::hardWrapText( const Uint32& maxWidth ) { - if ( hardWrapText( mString, maxWidth, mFontStyleConfig, mTabWidth ) ) + if ( hardWrapText( mString, maxWidth, mFontStyleConfig, mTabWidth, {}, mTextHints ) ) invalidate(); } diff --git a/src/eepp/graphics/textlayout.cpp b/src/eepp/graphics/textlayout.cpp index 3bffbcd64..6d12893be 100644 --- a/src/eepp/graphics/textlayout.cpp +++ b/src/eepp/graphics/textlayout.cpp @@ -273,6 +273,7 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, TextLayout& result = *resultPtr; result.paragraphs.push_back( {} ); ShapedTextParagraph* curParagraph = &result.paragraphs.back(); + curParagraph->wrapInfo.wraps.push_back( 0 ); struct GlyphDirCounter { int ltr{ 0 }; int rtl{ 0 }; @@ -425,6 +426,7 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, curParagraph->size.y = pen.y; result.paragraphs.push_back( {} ); curParagraph = &result.paragraphs.back(); + curParagraph->wrapInfo.wraps.push_back( segment.offset + segment.length - 1 ); } return true; } ); @@ -444,6 +446,7 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, prevChar = 0; result.paragraphs.push_back( {} ); curParagraph = &result.paragraphs.back(); + curParagraph->wrapInfo.wraps.push_back( i ); continue; } if ( curChar == '\r' ) { @@ -502,9 +505,6 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, if ( wrapMode != LineWrapMode::NoWrap && wrapWidth ) { wrapLayout( string, result, wrapMode, wrapWidth, vspace, keepIndentation, font, characterSize, style, tabWidth, outlineThickness, hspace ); - } else { - for ( auto& sp : result.paragraphs ) - sp.wrapInfo.wraps.push_back( 0 ); } sLayoutCache.put( hash, resultPtr ); @@ -537,13 +537,11 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result, const Float& outlineThickness, Float hspace ) { std::size_t paragraphCount = result.paragraphs.size(); - if ( paragraphCount ) - result.paragraphs[0].wrapInfo.wraps.push_back( 0 ); - for ( std::size_t paragraphIdx = 0; paragraphIdx < paragraphCount; paragraphIdx++ ) { ShapedTextParagraph& sp = result.paragraphs[paragraphIdx]; std::size_t shapedGlyphCount = sp.shapedGlyphs.size(); std::size_t lastSpace = std::string::npos; + std::size_t lastSpaceStringIdx = std::string::npos; Vector2f currentOffset( 0.f, 0.f ); Sizef maxSize{ 0, vspace }; @@ -561,21 +559,30 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result, if ( sg.position.x + sg.advance.x > wrapWidth ) { std::size_t breakIndex = idx; + std::size_t breakStringIdx = sg.stringIndex; + bool performWordWrap = ( lineWrapMode == LineWrapMode::Word && lastSpace != std::string::npos ); - ShapedGlyph& prevBreakGlyph = sp.shapedGlyphs[lastSpace]; - maxSize.x = - std::max( prevBreakGlyph.position.x + prevBreakGlyph.advance.x, maxSize.x ); + if ( performWordWrap ) { + ShapedGlyph& prevBreakGlyph = sp.shapedGlyphs[lastSpace]; + maxSize.x = + std::max( prevBreakGlyph.position.x + prevBreakGlyph.advance.x, maxSize.x ); + } // Break after the last space (start of the current word) - if ( performWordWrap ) + if ( performWordWrap ) { breakIndex = lastSpace + 1; + breakStringIdx = lastSpaceStringIdx + 1; + } if ( breakIndex > idx ) breakIndex = idx; - sp.wrapInfo.wraps.push_back( breakIndex ); + if ( breakStringIdx > string.size() ) + breakStringIdx = string.size(); + + sp.wrapInfo.wraps.push_back( breakStringIdx ); ShapedGlyph& breakGlyph = sp.shapedGlyphs[breakIndex]; Vector2f adjustment( -breakGlyph.position.x + sp.wrapInfo.paddingStart, vspace ); @@ -587,8 +594,10 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result, currentOffset += adjustment; lastSpace = std::string::npos; + lastSpaceStringIdx = std::string::npos; } else if ( LineWrap::isWrapChar( curChar ) ) { lastSpace = idx; + lastSpaceStringIdx = sg.stringIndex; } } diff --git a/src/eepp/graphics/textshaperun.cpp b/src/eepp/graphics/textshaperun.cpp index 748c5582c..20288e73b 100644 --- a/src/eepp/graphics/textshaperun.cpp +++ b/src/eepp/graphics/textshaperun.cpp @@ -36,6 +36,10 @@ std::size_t TextShapeRun::pos() const { return mIndex; } +std::size_t TextShapeRun::length() const { + return mLen; +} + void TextShapeRun::next() { if ( mIsRTL ) { mIndex -= mLen; diff --git a/src/eepp/system/color.cpp b/src/eepp/system/color.cpp index 5683556bf..8f89a2c51 100644 --- a/src/eepp/system/color.cpp +++ b/src/eepp/system/color.cpp @@ -199,6 +199,8 @@ RGB::RGB( Uint8 r, Uint8 g, Uint8 b ) : tRGB( r, g, b ) {} RGB::RGB( const tRGB& color ) : tRGB( color.r, color.g, color.b ) {} +RGB::RGB( const Color& color ) : tRGB( color.r, color.g, color.b ) {} + RGB::RGB( Uint32 Col ) { Col = BitOp::swapLE32( Col ); r = static_cast( Col >> 16 ); @@ -380,6 +382,10 @@ Colorf Color::toHsl() const { return hsl; } +RGB Color::toRGB() const { + return RGB( r, g, b ); +} + Color Color::clone() const { return Color( *this ); } diff --git a/src/tests/unit_tests/fontrendering.cpp b/src/tests/unit_tests/fontrendering.cpp index 23263757d..e1423505b 100644 --- a/src/tests/unit_tests/fontrendering.cpp +++ b/src/tests/unit_tests/fontrendering.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -965,3 +966,91 @@ UTEST( FontRendering, TextLayoutWrap ) { runTest(); } } + +UTEST( FontRendering, LineWrapInfo ) { + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + std::string string; + FileSystem::fileGet( "assets/textfiles/test-hard-wrap.uext", string ); + + UIApplication app( + WindowSettings( 1024, 650, "eepp - LineWrapInfo Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + Font* font = app.getUI()->getUIThemeManager()->getDefaultFont(); + Float width = app.getWindow()->getSize().getWidth(); + LineWrapMode mode = LineWrapMode::Word; + + LineWrapInfo lineWrapShaperDisabled; + LineWrapInfo lineWrapShaperEnabled; + { + UTEST_PRINT_STEP( "Text Shaper disabled" ); + BoolScopedOp op( Text::TextShaperEnabled, false ); + String str( string ); + lineWrapShaperDisabled = LineWrap::computeLineBreaks( string, font, 16, width, mode ); + } + + UTEST_PRINT_STEP( "Text Shaper enabled" ); + { + BoolScopedOp op( Text::TextShaperEnabled, true ); + String str( string ); + lineWrapShaperEnabled = LineWrap::computeLineBreaks( string, font, 16, width, mode ); + + { + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + String str( string ); + lineWrapShaperEnabled = LineWrap::computeLineBreaks( string, font, 16, width, mode ); + } + } + + EXPECT_VECTOREQ( lineWrapShaperDisabled.wraps, lineWrapShaperEnabled.wraps ); +} + +UTEST( FontRendering, TextHardWrap ) { + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + std::string string; + FileSystem::fileGet( "assets/textfiles/test-hard-wrap.uext", string ); + + const auto runTest = [&]() { + UIApplication app( + WindowSettings( 1024, 650, "eepp - Text Hard Wrap", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + auto colors = SyntaxColorScheme::getDefault(); + auto syntax = SyntaxDefinitionManager::instance()->getByLanguageName( "Markdown" ); + + app.getWindow()->setClearColor( colors.getEditorColor( "background"_sst ).toRGB() ); + app.getWindow()->clear(); + + Text text; + text.setFont( app.getUI()->getUIThemeManager()->getDefaultFont() ); + text.setFontSize( 16 ); + text.setColor( Color::Black ); + text.setString( string ); + text.hardWrapText( app.getWindow()->getSize().getWidth() ); + SyntaxTokenizer::tokenizeText( syntax, colors, &text ); + text.draw( 0, 0 ); + + compareImages( utest_state, utest_result, app.getWindow(), "eepp-text-hard-wrap" ); + }; + + UTEST_PRINT_STEP( "Text Shaper disabled" ); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } + + UTEST_PRINT_STEP( "Text Shaper enabled" ); + { + BoolScopedOp op( Text::TextShaperEnabled, true ); + runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); + } +} diff --git a/src/tests/unit_tests/utest.hpp b/src/tests/unit_tests/utest.hpp index 0915ece1f..b5f5e146d 100644 --- a/src/tests/unit_tests/utest.hpp +++ b/src/tests/unit_tests/utest.hpp @@ -1,55 +1,95 @@ #pragma once #include "utest.h" -#define UTEST_STDSTREQ(x, y, msg, is_assert) \ - UTEST_SURPRESS_WARNING_BEGIN do { \ - const std::string xEval = (x); \ - const std::string yEval = (y); \ - if (xEval != yEval) { \ - UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ - UTEST_PRINTF(" Expected : \"%s\"\n", xEval.c_str()); \ - UTEST_PRINTF(" Actual : \"%s\"\n", yEval.c_str()); \ - if (strlen(msg) > 0) { \ - UTEST_PRINTF(" Message : %s\n", msg); \ - } \ - *utest_result = UTEST_TEST_FAILURE; \ - if (is_assert) { \ - return; \ - } \ - } \ - } \ - while (0) \ - UTEST_SURPRESS_WARNING_END +#include +#include -#define EXPECT_STDSTREQ(x, y) UTEST_STDSTREQ(x, y, "", 0) -#define EXPECT_STDSTREQ_MSG(x, y, msg) UTEST_STDSTREQ(x, y, msg, 0) -#define ASSERT_STDSTREQ(x, y) UTEST_STDSTREQ(x, y, "", 1) -#define ASSERT_STDSTREQ_MSG(x, y, msg) UTEST_STDSTREQ(x, y, msg, 1) +template std::string vectorToString( const std::vector& vec ) { + std::ostringstream oss; + oss << "["; + bool first = true; + for ( const auto& element : vec ) { + if ( !first ) + oss << ", "; + oss << element; + first = false; + } + oss << "]"; + return oss.str(); +} -#define UTEST_STRINGEQ(x, y, msg, is_assert) \ - UTEST_SURPRESS_WARNING_BEGIN do { \ - const String xEval = (x); \ - const String yEval = (y); \ - if (xEval != yEval) { \ - UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ - UTEST_PRINTF(" Expected : \"%s\"\n", xEval.toUtf8().c_str()); \ - UTEST_PRINTF(" Actual : \"%s\"\n", yEval.toUtf8().c_str()); \ - if (strlen(msg) > 0) { \ - UTEST_PRINTF(" Message : %s\n", msg); \ - } \ - *utest_result = UTEST_TEST_FAILURE; \ - if (is_assert) { \ - return; \ - } \ - } \ - } \ - while (0) \ - UTEST_SURPRESS_WARNING_END +#define UTEST_STDSTREQ( x, y, msg, is_assert ) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const std::string xEval = ( x ); \ + const std::string yEval = ( y ); \ + if ( xEval != yEval ) { \ + UTEST_PRINTF( "%s:%i: Failure\n", __FILE__, __LINE__ ); \ + UTEST_PRINTF( " Expected : \"%s\"\n", xEval.c_str() ); \ + UTEST_PRINTF( " Actual : \"%s\"\n", yEval.c_str() ); \ + if ( strlen( msg ) > 0 ) { \ + UTEST_PRINTF( " Message : %s\n", msg ); \ + } \ + *utest_result = UTEST_TEST_FAILURE; \ + if ( is_assert ) { \ + return; \ + } \ + } \ + } \ + while ( 0 ) \ + UTEST_SURPRESS_WARNING_END -#define EXPECT_STRINGEQ(x, y) UTEST_STRINGEQ(x, y, "", 0) -#define EXPECT_STRINGEQ_MSG(x, y, msg) UTEST_STRINGEQ(x, y, msg, 0) -#define ASSERT_STRINGEQ(x, y) UTEST_STRINGEQ(x, y, "", 1) -#define ASSERT_STRINGEQ_MSG(x, y, msg) UTEST_STRINGEQ(x, y, msg, 1) +#define EXPECT_STDSTREQ( x, y ) UTEST_STDSTREQ( x, y, "", 0 ) +#define EXPECT_STDSTREQ_MSG( x, y, msg ) UTEST_STDSTREQ( x, y, msg, 0 ) +#define ASSERT_STDSTREQ( x, y ) UTEST_STDSTREQ( x, y, "", 1 ) +#define ASSERT_STDSTREQ_MSG( x, y, msg ) UTEST_STDSTREQ( x, y, msg, 1 ) -#define UTEST_PRINT_INFO(str) UTEST_PRINTF( "\033[32m[ INFO ]\033[0m \t%s\n", str ) -#define UTEST_PRINT_STEP(str) UTEST_PRINTF( "\033[32m[ SUB-STEP ]\033[0m \t%s\n", str ) +#define UTEST_STRINGEQ( x, y, msg, is_assert ) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const String xEval = ( x ); \ + const String yEval = ( y ); \ + if ( xEval != yEval ) { \ + UTEST_PRINTF( "%s:%i: Failure\n", __FILE__, __LINE__ ); \ + UTEST_PRINTF( " Expected : \"%s\"\n", xEval.toUtf8().c_str() ); \ + UTEST_PRINTF( " Actual : \"%s\"\n", yEval.toUtf8().c_str() ); \ + if ( strlen( msg ) > 0 ) { \ + UTEST_PRINTF( " Message : %s\n", msg ); \ + } \ + *utest_result = UTEST_TEST_FAILURE; \ + if ( is_assert ) { \ + return; \ + } \ + } \ + } \ + while ( 0 ) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_STRINGEQ( x, y ) UTEST_STRINGEQ( x, y, "", 0 ) +#define EXPECT_STRINGEQ_MSG( x, y, msg ) UTEST_STRINGEQ( x, y, msg, 0 ) +#define ASSERT_STRINGEQ( x, y ) UTEST_STRINGEQ( x, y, "", 1 ) +#define ASSERT_STRINGEQ_MSG( x, y, msg ) UTEST_STRINGEQ( x, y, msg, 1 ) + +#define UTEST_PRINT_INFO( str ) UTEST_PRINTF( "\033[32m[ INFO ]\033[0m \t%s\n", str ) +#define UTEST_PRINT_STEP( str ) UTEST_PRINTF( "\033[32m[ SUB-STEP ]\033[0m \t%s\n", str ) + +#define UTEST_VECTOREQ( xEval, yEval, msg, is_assert ) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + if ( xEval != yEval ) { \ + UTEST_PRINTF( "%s:%i: Failure\n", __FILE__, __LINE__ ); \ + UTEST_PRINTF( " Expected : \"%s\"\n", vectorToString( xEval ).c_str() ); \ + UTEST_PRINTF( " Actual : \"%s\"\n", vectorToString( yEval ).c_str() ); \ + if ( strlen( msg ) > 0 ) { \ + UTEST_PRINTF( " Message : %s\n", msg ); \ + } \ + *utest_result = UTEST_TEST_FAILURE; \ + if ( is_assert ) { \ + return; \ + } \ + } \ + } \ + while ( 0 ) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_VECTOREQ( x, y ) UTEST_VECTOREQ( x, y, "", 0 ) +#define EXPECT_VECTOREQ_MSG( x, y, msg ) UTEST_VECTOREQ( x, y, msg, 0 ) +#define ASSERT_VECTOREQ( x, y ) UTEST_VECTOREQ( x, y, "", 1 ) +#define ASSERT_VECTOREQ_MSG( x, y, msg ) UTEST_VECTOREQ( x, y, msg, 1 )