From ec47dffa3028dd73c9c14d48d22ca0ab06a2452b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 3 Feb 2026 01:41:15 -0300 Subject: [PATCH] Several fixes in soft-wrap implementation. Added soft-wrap support in UITextView and UITooltip. Added more soft-wrap tests, now testing also selection. --- .../eepp-textview-wrapped-selection.webp | Bin 0 -> 9020 bytes include/eepp/graphics/text.hpp | 7 +- include/eepp/ui/uitextview.hpp | 12 +- src/eepp/graphics/linewrap.cpp | 5 +- src/eepp/graphics/text.cpp | 85 ++++++++++-- src/eepp/graphics/textlayout.cpp | 10 +- src/eepp/ui/doc/documentview.cpp | 3 +- src/eepp/ui/uicodeeditor.cpp | 2 +- src/eepp/ui/uitextview.cpp | 59 ++++----- src/eepp/ui/uitooltip.cpp | 8 +- src/tests/ui_perf_test/ui_perf_test.cpp | 12 +- src/tests/unit_tests/fontrendering.cpp | 125 +++++++++++++++++- 12 files changed, 262 insertions(+), 66 deletions(-) create mode 100644 bin/unit_tests/assets/fontrendering/eepp-textview-wrapped-selection.webp diff --git a/bin/unit_tests/assets/fontrendering/eepp-textview-wrapped-selection.webp b/bin/unit_tests/assets/fontrendering/eepp-textview-wrapped-selection.webp new file mode 100644 index 0000000000000000000000000000000000000000..7ff77b429e2081df7f59f5c78e4dda0da4a3ba8f GIT binary patch literal 9020 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 getSelectionRects( size_t selectionStartIndex, size_t selectionEndIndex ); + protected: struct VertexCoords { Vector2f texCoords; @@ -383,7 +388,7 @@ class EE_API Text { mutable bool mColorsNeedUpdate : 1 { false }; mutable bool mContainsColorEmoji : 1 { false }; mutable bool mVisualLinesNeedUpdate : 1 { true }; - mutable bool mCachedWidthNeedUpdate: 1 { true }; + mutable bool mCachedWidthNeedUpdate : 1 { true }; bool mTabStops : 1 { false }; bool mLineWrapKeepIndentation : 1 { false }; diff --git a/include/eepp/ui/uitextview.hpp b/include/eepp/ui/uitextview.hpp index ef43881a8..2b424390c 100644 --- a/include/eepp/ui/uitextview.hpp +++ b/include/eepp/ui/uitextview.hpp @@ -124,6 +124,10 @@ class EE_API UITextView : public UIWidget { bool isWordWrap() const; + std::pair getSelection() const; + + void setSelection( std::pair sel ); + protected: Text* mTextCache; String mString; @@ -132,13 +136,7 @@ class EE_API UITextView : public UIWidget { Int32 mSelCurInit; Int32 mSelCurEnd; Uint32 mTextDrawHints{ 0 }; - struct SelPosCache { - SelPosCache( Vector2f ip, Vector2f ep ) : initPos( ip ), endPos( ep ) {} - - Vector2f initPos; - Vector2f endPos; - }; - std::vector mSelPosCache; + std::vector mSelRectsCache; Int32 mLastSelCurInit; Int32 mLastSelCurEnd; bool mSelecting; diff --git a/src/eepp/graphics/linewrap.cpp b/src/eepp/graphics/linewrap.cpp index 44cb70452..f044cc0f3 100644 --- a/src/eepp/graphics/linewrap.cpp +++ b/src/eepp/graphics/linewrap.cpp @@ -74,7 +74,7 @@ LineWrap::computeLineBreaksInternal( const String::View& string, Font* font, Uin LineWrapType info; info.wraps.push_back( 0 ); - if ( string.empty() || nullptr == font || mode == LineWrapMode::NoWrap || maxWidth == 0 ) { + if ( string.empty() || nullptr == font ) { if constexpr ( std::is_same_v ) { info.wrapsWidth.push_back( 0 ); } @@ -143,6 +143,7 @@ LineWrap::computeLineBreaksInternal( const String::View& string, Font* font, Uin size_t lastSpace = 0; Uint32 prevChar = 0; size_t idx = 0; + bool hasWrap = maxWidth > 0 && mode != LineWrapMode::NoWrap; for ( const auto& curChar : string ) { if ( curChar == '\n' ) { @@ -175,7 +176,7 @@ LineWrap::computeLineBreaksInternal( const String::View& string, Font* font, Uin xoffset += w; - if ( xoffset > maxWidth ) { + if ( hasWrap && xoffset > maxWidth ) { if ( mode == LineWrapMode::Word && lastSpace ) { if constexpr ( std::is_same_v ) { info.wrapsWidth.push_back( std::ceil( lastWordWrapWidth ) ); diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index 888b00263..cac8d28a0 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1938,21 +1939,11 @@ void Text::ensureGeometryUpdate() { } } - // Helper lambda to check if index starts a soft-wrapped line (not a real newline) auto isSoftWrapLineStart = [this, &useSoftWrap, ¤tVisualLine]( Int64 idx ) -> bool { - if ( !useSoftWrap || currentVisualLine + 1 >= mVisualLines.size() ) - return false; - // Check if this is the start of the next visual line - if ( idx == mVisualLines[currentVisualLine + 1] ) { - // It's a soft wrap if the previous char wasn't a newline - if ( idx > 0 && mString[idx - 1] != '\n' ) { - return true; - } - } - return false; + return !( !useSoftWrap || currentVisualLine + 1 >= mVisualLines.size() ) && + idx == mVisualLines[currentVisualLine + 1] && idx > 0 && mString[idx] != '\n'; }; - // Helper to update alignment for current visual line auto updateAlignmentForLine = [this, ¢erDiffX, &line]() { switch ( Font::getHorizontalAlign( mAlign ) ) { case TEXT_ALIGN_CENTER: @@ -2179,6 +2170,8 @@ void Text::ensureGeometryUpdate() { // For soft wrap, the width cache was already handled by ensureVisualLinesUpdate if ( useSoftWrap && mCachedWidthNeedUpdate ) { mCachedWidthNeedUpdate = false; + if ( !mLinesWidth.empty() ) + mCachedWidth = *std::max_element( mLinesWidth.begin(), mLinesWidth.end() ); } } @@ -2785,4 +2778,72 @@ size_t Text::findVisualLineFromCharIndex( size_t charIndex ) { return 0; } +std::vector Text::getSelectionRects( size_t selectionStartIndex, size_t selectionEndIndex ) { + std::vector rects; + + if ( selectionStartIndex == selectionEndIndex || !mFontStyleConfig.Font ) + return rects; + + if ( selectionStartIndex > selectionEndIndex ) + std::swap( selectionStartIndex, selectionEndIndex ); + + ensureVisualLinesUpdate(); + cacheWidth(); + + size_t startLine = findVisualLineFromCharIndex( selectionStartIndex ); + size_t endLine = findVisualLineFromCharIndex( selectionEndIndex ); + Float hspace = + mFontStyleConfig.Font + ->getGlyph( ' ', mFontStyleConfig.CharacterSize, mFontStyleConfig.Style & Text::Bold, + mFontStyleConfig.Style & Text::Italic ) + .advance; + Float vspace = static_cast( + mFontStyleConfig.Font->getLineSpacing( mFontStyleConfig.CharacterSize ) ); + + for ( size_t i = startLine; i <= endLine; ++i ) { + Float top = i * vspace; + Float bottom = top + vspace; + Float left = 0; + Float right = 0; + Float centerDiffX = 0; + + if ( i < mLinesWidth.size() ) { + switch ( Font::getHorizontalAlign( mAlign ) ) { + case TEXT_ALIGN_CENTER: + centerDiffX = std::trunc( ( mCachedWidth - mLinesWidth[i] ) * 0.5f ); + break; + case TEXT_ALIGN_RIGHT: + centerDiffX = mCachedWidth - mLinesWidth[i]; + break; + } + } + + // Calculate Left + if ( i == startLine ) { + left = findCharacterPos( selectionStartIndex ).x; + } else { + left = centerDiffX; + } + + // Calculate Right + if ( i == endLine ) { + // If it's a newline character, we select a small chunk to indicate the newline + // selection + if ( selectionEndIndex < mString.size() && mString[selectionEndIndex] == '\n' ) { + right = findCharacterPos( selectionEndIndex ).x + hspace; + } else { + right = findCharacterPos( selectionEndIndex ).x; + } + } else { + right = centerDiffX + ( i < mLinesWidth.size() ? mLinesWidth[i] : 0 ); + } + + if ( left != right ) { + rects.push_back( Rectf( left, top, right, bottom ) ); + } + } + + return rects; +} + }} // namespace EE::Graphics diff --git a/src/eepp/graphics/textlayout.cpp b/src/eepp/graphics/textlayout.cpp index 3fe5d160a..fcd9e49fb 100644 --- a/src/eepp/graphics/textlayout.cpp +++ b/src/eepp/graphics/textlayout.cpp @@ -545,6 +545,8 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result, const Float& outlineThickness, Float hspace ) { std::size_t paragraphCount = result.paragraphs.size(); + result.size = Sizef::Zero; + for ( std::size_t paragraphIdx = 0; paragraphIdx < paragraphCount; paragraphIdx++ ) { ShapedTextParagraph& sp = result.paragraphs[paragraphIdx]; std::size_t shapedGlyphCount = sp.shapedGlyphs.size(); @@ -614,6 +616,9 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result, } if ( sp.wrapInfo.wrapsWidth.empty() ) { + if ( shapedGlyphCount ) + maxSize = sp.shapedGlyphs.back().position + sp.shapedGlyphs.back().advance; + // Restore the original wraps which are the paragraph wraps (no wrapping occurred) sp.wrapInfo.wrapsWidth = std::move( wrapsWidth ); } else if ( !sp.shapedGlyphs.empty() ) { @@ -622,9 +627,10 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result, } sp.size = maxSize; - } - result.size = result.paragraphs[result.paragraphs.size() - 1].size; + result.size = { std::max( sp.size.x, result.size.x ), + std::max( sp.size.y, result.size.y ) }; + } } } // namespace EE::Graphics diff --git a/src/eepp/ui/doc/documentview.cpp b/src/eepp/ui/doc/documentview.cpp index 9f21c6d18..a974d636b 100644 --- a/src/eepp/ui/doc/documentview.cpp +++ b/src/eepp/ui/doc/documentview.cpp @@ -272,7 +272,8 @@ TextRange DocumentView::getVisibleIndexRange( VisibleIndex visibleIndex ) const Int64 idx = static_cast( visibleIndex ); auto start = getVisibleIndexPosition( visibleIndex ); auto end = start; - if ( idx + 1 < static_cast( mVisibleLines.size() ) && + eeASSERT( visibleIndex >= static_cast( 0 ) ); + if ( idx >= 0 && idx + 1 < static_cast( mVisibleLines.size() ) && mVisibleLines[idx + 1].line() == start.line() ) { end.setColumn( mVisibleLines[idx + 1].column() ); } else { diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 322c9a708..84caef24a 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -4701,7 +4701,7 @@ String UICodeEditor::checkMouseOverLink( const Vector2i& position, bool checkMod if ( !mInteractiveLinks || ( checkModifiers && !getInput()->isKeyModPressed() ) ) return resetLinkOver( position ); - TextPosition pos( resolveScreenPosition( position.asFloat(), false ) ); + TextPosition pos( resolveScreenPosition( position.asFloat() ) ); if ( pos.line() >= (Int64)mDoc->linesCount() ) return resetLinkOver( position ); diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index eda5991b0..1b535b028 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -323,6 +323,9 @@ UITextView* UITextView::setSelectionBackColor( const Color& color ) { } void UITextView::autoWrap() { + mTextCache->setLineWrapMode( mFlags & UI_WORD_WRAP ? LineWrapMode::Word + : LineWrapMode::NoWrap ); + if ( mFlags & UI_WORD_WRAP ) { wrapText( mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right ); } @@ -333,7 +336,8 @@ void UITextView::wrapText( const Uint32& maxWidth ) { mTextCache->setString( mString ); } - mTextCache->hardWrapText( maxWidth ); + mTextCache->setLineWrapMode( LineWrapMode::Word ); + mTextCache->setMaxWrapWidth( maxWidth ); invalidateDraw(); } @@ -564,46 +568,26 @@ void UITextView::drawSelection( Text* textCache ) { return; } - Int32 lastEnd; - Vector2f initPos, endPos; - if ( mLastSelCurInit != selCurInit() || mLastSelCurEnd != selCurEnd() ) { - mSelPosCache.clear(); + mSelRectsCache.clear(); mLastSelCurInit = selCurInit(); mLastSelCurEnd = selCurEnd(); - - do { - initPos = textCache->findCharacterPos( init ); - lastEnd = textCache->getString().find_first_of( '\n', init ); - - if ( lastEnd < end && -1 != lastEnd ) { - endPos = textCache->findCharacterPos( lastEnd ); - init = lastEnd + 1; - } else { - endPos = textCache->findCharacterPos( end ); - lastEnd = end; - } - - mSelPosCache.push_back( SelPosCache( initPos, endPos ) ); - } while ( end != lastEnd ); + mSelRectsCache = mTextCache->getSelectionRects( selCurInit(), selCurEnd() ); } - if ( !mSelPosCache.empty() ) { + if ( !mSelRectsCache.empty() ) { + Vector2f initPos, endPos; Primitives P; P.setColor( mFontStyleConfig.FontSelectionBackColor ); - Float vspace = textCache->getFont()->getLineSpacing( mTextCache->getCharacterSize() ); - Float height = mSize.y - mPaddingPx.Top - mPaddingPx.Bottom; - Float offsetY = eefloor( ( height - mTextCache->getTextHeight() ) * 0.5f ); + for ( size_t i = 0; i < mSelRectsCache.size(); i++ ) { + initPos = mSelRectsCache[i].getPosition(); + endPos = mSelRectsCache[i].getPosition() + mSelRectsCache[i].getSize(); - for ( size_t i = 0; i < mSelPosCache.size(); i++ ) { - initPos = mSelPosCache[i].initPos; - endPos = mSelPosCache[i].endPos; - - P.drawRectangle( - Rectf( mScreenPos.x + initPos.x + mRealAlignOffset.x + mPaddingPx.Left, - mScreenPos.y + initPos.y + offsetY + mPaddingPx.Top, - mScreenPos.x + endPos.x + mRealAlignOffset.x + mPaddingPx.Left, - mScreenPos.y + endPos.y + offsetY + mPaddingPx.Top + vspace ) ); + P.drawRectangle( Rectf( + mScreenPos.x + initPos.x + mRealAlignOffset.x + mPaddingPx.Left, + mScreenPos.y + initPos.y + mRealAlignOffset.y + mPaddingPx.Top, + mScreenPos.x + endPos.x + mRealAlignOffset.x + mPaddingPx.Left, + mScreenPos.y + endPos.y + mRealAlignOffset.y + mPaddingPx.Top ) ); } } } @@ -637,6 +621,15 @@ void UITextView::setFontStyleConfig( const UIFontStyleConfig& fontStyleConfig ) onFontStyleChanged(); } +std::pair UITextView::getSelection() const { + return { mSelCurInit, mSelCurEnd }; +} + +void UITextView::setSelection( std::pair sel ) { + selCurInit( std::clamp( sel.first, 0, (Int32)mString.size() ) ); + selCurEnd( std::clamp( sel.second, 0, (Int32)mString.size() ) ); +} + void UITextView::selCurInit( const Int32& init ) { if ( mSelCurInit != init ) { mSelCurInit = init; diff --git a/src/eepp/ui/uitooltip.cpp b/src/eepp/ui/uitooltip.cpp index 9ef444d11..27e77f105 100644 --- a/src/eepp/ui/uitooltip.cpp +++ b/src/eepp/ui/uitooltip.cpp @@ -159,7 +159,7 @@ void UITooltip::draw() { UINode::draw(); if ( mTextCache->getTextWidth() ) { - mTextCache->setAlign( getFlags() ); + mTextCache->setAlign( getHorizontalAlign() | getVerticalAlign() ); mTextCache->draw( std::trunc( mScreenPos.x ) + (int)mAlignOffset.x, std::trunc( mScreenPos.y ) + (int)mAlignOffset.y, Vector2f::One, 0.f, getBlendMode() ); @@ -615,6 +615,9 @@ void UITooltip::onAlphaChange() { } void UITooltip::autoWrap() { + mTextCache->setLineWrapMode( mFlags & UI_WORD_WRAP ? LineWrapMode::Word + : LineWrapMode::NoWrap ); + if ( mFlags & UI_WORD_WRAP && !mMaxWidthEq.empty() ) { Float length = lengthFromValue( mMaxWidthEq, CSS::PropertyRelativeTarget::ContainingBlockWidth ); @@ -627,7 +630,8 @@ void UITooltip::wrapText( const Uint32& maxWidth ) { mTextCache->setString( mStringBuffer ); } - mTextCache->hardWrapText( maxWidth ); + mTextCache->setLineWrapMode( LineWrapMode::Word ); + mTextCache->setMaxWrapWidth( maxWidth ); invalidateDraw(); } diff --git a/src/tests/ui_perf_test/ui_perf_test.cpp b/src/tests/ui_perf_test/ui_perf_test.cpp index e1e898c7d..aacf2b01e 100644 --- a/src/tests/ui_perf_test/ui_perf_test.cpp +++ b/src/tests/ui_perf_test/ui_perf_test.cpp @@ -161,8 +161,11 @@ EE_MAIN_FUNC int main( int, char*[] ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); auto ll = UILinearLayout::NewVertical(); ll->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + auto editor = UITextView::New(); + /* auto editor = UITextEdit::New(); editor->setShowLineNumber( false ); + */ editor->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); editor->setParent( ll ); editor->setFontSize( PixelDensity::dpToPx( 12 ) ); @@ -170,7 +173,8 @@ EE_MAIN_FUNC int main( int, char*[] ) { FontTrueType::New( "arabic", "unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf" ) ); FontManager::instance()->addFallbackFont( FontTrueType::New( "NotoSerifBengali-Regular", "unit_tests/assets/fonts/NotoSansBengali-Regular.ttf" ) ); - editor->setLineWrapMode( LineWrapMode::Word ); + editor->setWordWrap( true ); + // editor->setLineWrapMode( LineWrapMode::Word ); // editor->setFont( FontManager::instance()->getByName( "monospace" ) ); // editor->loadFromFile( "unit_tests/assets/textfiles/test-arabic-simple.uext" ); // editor->loadFromFile( "unit_tests/assets/textfiles/test-arabic.uext" ); @@ -179,7 +183,11 @@ EE_MAIN_FUNC int main( int, char*[] ) { // editor->loadFromFile( "unit_tests/assets/textformat/english.utf8.lf.nobom.txt" ); // editor->loadFromFile( "unit_tests/assets/textfiles/test-arabic-mixed.uext" ); // editor->loadFromFile( "unit_tests/assets/textfiles/test-mixed-text.uext" ); - editor->loadFromFile( "unit_tests/assets/textfiles/lorem-ipsum.uext" ); + // editor->loadFromFile( "unit_tests/assets/textfiles/lorem-ipsum.uext" ); + std::string buffer; + FileSystem::fileGet( "unit_tests/assets/textfiles/lorem-ipsum.uext", buffer ); + editor->setText( buffer ); + editor->setTextSelection( true ); editor->setFont( app.getUI()->getUIThemeManager()->getDefaultFont() ); editor->on( Event::KeyUp, [&]( const Event* event ) { diff --git a/src/tests/unit_tests/fontrendering.cpp b/src/tests/unit_tests/fontrendering.cpp index 5d3e7994e..b8daf6cf3 100644 --- a/src/tests/unit_tests/fontrendering.cpp +++ b/src/tests/unit_tests/fontrendering.cpp @@ -1089,6 +1089,46 @@ UTEST( FontRendering, TextHardWrap ) { } } +UTEST( FontRendering, UITextViewWrappedSelection ) { + const auto runTest = [&]() { + UIApplication app( + WindowSettings( 1024, 650, "eepp - TextView Wrapped Selection", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), + 1.5f ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + std::string buffer; + FileSystem::fileGet( "assets/textfiles/lorem-ipsum.uext", buffer ); + auto textView = UITextView::New(); + textView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + textView->setPixelsSize( app.getUI()->getPixelsSize() ); + textView->setText( buffer ); + textView->setWordWrap( true ); + textView->setTextSelection( true ); + textView->setSelection( { 51, 286 } ); + SceneManager::instance()->update(); + SceneManager::instance()->draw(); + compareImages( utest_state, utest_result, app.getWindow(), + "eepp-textview-wrapped-selection" ); + }; + + 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(); + } +} + UTEST( FontRendering, TextSoftWrapPos ) { const auto runTest = [&]() { UIApplication app( @@ -1109,16 +1149,16 @@ UTEST( FontRendering, TextSoftWrapPos ) { text.setMaxWrapWidth( 200.f ); Vector2f pos = text.findCharacterPos( 30 ); - EXPECT_GT( pos.y, 0 ); + EXPECT_GT( pos.y, 0.f ); Float vspace = text.getFont()->getLineSpacing( text.getCharacterSize() ); Vector2i queryPos( 10, (int)vspace + 5 ); Int32 foundIndex = text.findCharacterFromPos( queryPos ); - EXPECT_GT( foundIndex, 14 ); + EXPECT_GT( foundIndex, (Int32)14 ); Vector2f foundPos = text.findCharacterPos( foundIndex ); - EXPECT_GT( foundPos.y, 0 ); + EXPECT_GT( foundPos.y, 0.f ); }; UTEST_PRINT_STEP( "Text Shaper disabled" ); @@ -1137,3 +1177,82 @@ UTEST( FontRendering, TextSoftWrapPos ) { runTest(); } } + +UTEST( FontRendering, TextSelection ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 650, "eepp - Text Selection", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ) ); + ASSERT_TRUE_MSG( win->isOpen(), "Failed to create Window" ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + Text::TextShaperEnabled = false; + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + bool loaded = font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + ASSERT_TRUE( loaded ); + FontFamily::loadFromRegular( font ); + + FontStyleConfig config; + config.Font = font; + config.CharacterSize = 20; + config.FontColor = Color::Black; + config.Style = Text::Regular; + + String txt( "Line 1\nLine 2 is longer\nLine 3" ); + + Text text; + text.setStyleConfig( config ); + text.setString( txt ); + + // Test 1: Single line selection (Line 1) + { + std::vector rects = text.getSelectionRects( 0, 4 ); // "Line" + EXPECT_EQ( 1ul, rects.size() ); + if ( !rects.empty() ) { + EXPECT_EQ( 0, rects[0].Top ); + EXPECT_GT( rects[0].getWidth(), 0 ); + EXPECT_EQ( text.findCharacterPos( 0 ).x, rects[0].Left ); + EXPECT_EQ( text.findCharacterPos( 4 ).x, rects[0].Right ); + } + } + + // Test 2: Multi-line selection (Line 1 to Line 2) + { + // "Line 1\nLine 2" -> Indices: "Line 1" (0-5), "\n" (6), "Line 2" (7-12) + // Select from index 2 ("n" in "Line 1") to index 9 ("i" in "Line 2") + std::vector rects = text.getSelectionRects( 2, 9 ); + EXPECT_EQ( 2ul, rects.size() ); + if ( rects.size() >= 2 ) { + // First line rect: From index 2 to end of line 1 + EXPECT_EQ( text.findCharacterPos( 2 ).x, rects[0].Left ); + EXPECT_GT( rects[0].Right, rects[0].Left ); + + // Second line rect: From start of line 2 to index 9 + EXPECT_EQ( 0, rects[1].Left ); // Left aligned + EXPECT_EQ( text.findCharacterPos( 9 ).x, rects[1].Right ); + } + } + + // Test 3: Full selection + { + std::vector rects = text.getSelectionRects( 0, txt.size() ); + EXPECT_EQ( 3ul, rects.size() ); + } + + // Test 4: Soft wrap + { + text.setLineWrapMode( LineWrapMode::Word ); + text.setMaxWrapWidth( 50 ); // Force wrap + + text.setString( "This is a very long string that should wrap multiple times." ); + // Ensure layout is updated + text.getVisualLineCount(); + + EXPECT_GT( text.getVisualLineCount(), (Uint32)1 ); + + std::vector rects = text.getSelectionRects( 0, text.getString().size() ); + EXPECT_EQ( (size_t)text.getVisualLineCount(), rects.size() ); + } + + Engine::destroySingleton(); +}