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 000000000..7ff77b429 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-textview-wrapped-selection.webp differ diff --git a/include/eepp/graphics/text.hpp b/include/eepp/graphics/text.hpp index 1c35c8aaf..f4dc4d2aa 100644 --- a/include/eepp/graphics/text.hpp +++ b/include/eepp/graphics/text.hpp @@ -368,6 +368,11 @@ class EE_API Text { /** Finds the visual line index that contains the given character index. */ size_t findVisualLineFromCharIndex( size_t charIndex ); + /** @return A list of rectangles that cover the selection of the string, each rectangle + * has the line spacing height and covers the width of the selection. + */ + std::vector 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(); +}