diff --git a/.github/workflows/eepp-macos-build-check.yml b/.github/workflows/eepp-macos-build-check.yml index bf01834a4..f3ba3b148 100644 --- a/.github/workflows/eepp-macos-build-check.yml +++ b/.github/workflows/eepp-macos-build-check.yml @@ -20,7 +20,7 @@ jobs: - name: Build run: | premake5 --disable-static-build gmake - make -C make/macosx/ -e config=release_arm64 + make -C make/macosx/ -j$(sysctl -n hw.ncpu) -e config=release_arm64 - name: Unit Tests run: | cd bin/unit_tests diff --git a/bin/unit_tests/assets/fontrendering/eepp-fonts.webp b/bin/unit_tests/assets/fontrendering/eepp-fonts.webp index 5ddbeb0c6..6fa72c312 100644 Binary files a/bin/unit_tests/assets/fontrendering/eepp-fonts.webp and b/bin/unit_tests/assets/fontrendering/eepp-fonts.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-layout-wrap.webp b/bin/unit_tests/assets/fontrendering/eepp-text-layout-wrap.webp new file mode 100644 index 000000000..ddb4a220c Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-layout-wrap.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-wrap.webp b/bin/unit_tests/assets/fontrendering/eepp-text-wrap.webp index e40b70d2b..787f49d59 100644 Binary files a/bin/unit_tests/assets/fontrendering/eepp-text-wrap.webp and b/bin/unit_tests/assets/fontrendering/eepp-text-wrap.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-ui-text-test.webp b/bin/unit_tests/assets/fontrendering/eepp-ui-text-test.webp index d97d432da..5330d0f18 100644 Binary files a/bin/unit_tests/assets/fontrendering/eepp-ui-text-test.webp and b/bin/unit_tests/assets/fontrendering/eepp-ui-text-test.webp differ diff --git a/include/eepp/graphics/textlayout.hpp b/include/eepp/graphics/textlayout.hpp index 302b1d881..8da9ab157 100644 --- a/include/eepp/graphics/textlayout.hpp +++ b/include/eepp/graphics/textlayout.hpp @@ -15,6 +15,7 @@ class Font; struct ShapedTextParagraph { std::vector shapedGlyphs; + LineWrapInfo wrapInfo; Sizef size; }; @@ -35,21 +36,23 @@ class EE_API TextLayout { const Uint32& style, const Uint32& tabWidth = 4, const Float& outlineThickness = 0.f, std::optional tabOffset = {}, Uint32 textDrawHints = 0, - TextDirection baseDirection = TextDirection::LeftToRight ); + TextDirection baseDirection = TextDirection::LeftToRight, + LineWrapMode lineWrapMode = LineWrapMode::NoWrap, Uint32 wrapWidth = 0, + bool keepIndentation = false, Float initialXOffset = 0 ); static Cache layout( const String::View& string, Font* font, const Uint32& fontSize, const Uint32& style, const Uint32& tabWidth = 4, const Float& outlineThickness = 0.f, std::optional tabOffset = {}, Uint32 textDrawHints = 0, - TextDirection baseDirection = TextDirection::LeftToRight ); + TextDirection baseDirection = TextDirection::LeftToRight, + LineWrapMode lineWrapMode = LineWrapMode::NoWrap, Uint32 wrapWidth = 0, + bool keepIndentation = false, Float initialXOffset = 0 ); protected: - template - static Cache layout( const StringType& string, Font* font, const Uint32& fontSize, - const Uint32& style, const Uint32& tabWidth = 4, - const Float& outlineThickness = 0.f, std::optional tabOffset = {}, - Uint32 textDrawHints = 0, - TextDirection baseDirection = TextDirection::LeftToRight ); + static void wrapLayout( const String::View& string, TextLayout&, LineWrapMode lineWrapMode, + Float wrapWidth, Float vspace, bool keepIndentation, Font* font, + const Uint32& characterSize, const Uint32& fontStyle, + const Uint32& tabWidth, const Float& outlineThickness, Float hspace ); }; } // namespace EE::Graphics diff --git a/src/eepp/graphics/linewrap.cpp b/src/eepp/graphics/linewrap.cpp index 8047dba10..79a3c6d31 100644 --- a/src/eepp/graphics/linewrap.cpp +++ b/src/eepp/graphics/linewrap.cpp @@ -73,9 +73,32 @@ LineWrapInfo LineWrap::computeLineBreaks( const String::View& string, Font* font Float initialXOffset ) { LineWrapInfo info; info.wraps.push_back( 0 ); - if ( string.empty() || nullptr == font || mode == LineWrapMode::NoWrap ) + + if ( string.empty() || nullptr == font || mode == LineWrapMode::NoWrap || maxWidth == 0 ) return info; +#ifdef EE_TEXT_SHAPER_ENABLED + if ( Text::TextShaperEnabled && font->getType() == FontType::TTF && + !Text::canSkipShaping( textDrawHints ) ) { + auto layout = TextLayout::layout( + string, font, characterSize, fontStyle, tabWidth, outlineThickness, + tabStops ? initialXOffset : std::optional{}, textDrawHints, + TextDirection::LeftToRight, mode, maxWidth, keepIndentation, initialXOffset ); + LineWrapInfo info; + if ( layout->paragraphs.empty() ) + return info; + + info.paddingStart = layout->paragraphs.front().wrapInfo.paddingStart; + + for ( auto& paragraph : layout->paragraphs ) { + for ( const auto& wrap : paragraph.wrapInfo.wraps ) + info.wraps.push_back( wrap ); + } + + return info; + } +#endif + bool bold = ( fontStyle & Text::Style::Bold ) != 0; bool italic = ( fontStyle & Text::Style::Italic ) != 0; Float hspace = @@ -129,10 +152,10 @@ LineWrapInfo LineWrap::computeLineBreaks( const String::View& string, Font* font if ( xoffset > maxWidth ) { if ( mode == LineWrapMode::Word && lastSpace ) { info.wraps.push_back( lastSpace + 1 ); - xoffset = w + info.paddingStart + ( xoffset - lastWidth ); + xoffset = info.paddingStart + ( xoffset - lastWidth ); } else { info.wraps.push_back( idx ); - xoffset = w + info.paddingStart; + xoffset = info.paddingStart; } lastSpace = 0; } else if ( isWrapChar( curChar ) ) { diff --git a/src/eepp/graphics/textlayout.cpp b/src/eepp/graphics/textlayout.cpp index f89d5d5f6..3bffbcd64 100644 --- a/src/eepp/graphics/textlayout.cpp +++ b/src/eepp/graphics/textlayout.cpp @@ -218,25 +218,30 @@ static void shapeAndRun( TextLayout& result, const String& string, FontTrueType* #endif -template -static inline Uint64 textLayoutHash( const StringType& string, Font* font, +static inline Uint64 textLayoutHash( const String::View& string, Font* font, const Uint32& characterSize, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, - std::optional tabOffset, TextDirection direction ) { - return hashCombine( std::hash()( string ), std::hash()( font ), + std::optional tabOffset, TextDirection direction, + LineWrapMode wrapMode, Uint32 wrapWidth, + bool keepIndentation ) { + return hashCombine( std::hash()( string ), std::hash()( font ), std::hash()( characterSize ), std::hash()( style ), std::hash()( tabWidth ), std::hash()( outlineThickness ), std::hash>()( tabOffset ), std::hash>()( - static_cast>( direction ) ) ); + static_cast>( direction ) ), + std::hash>()( + static_cast>( wrapMode ) ), + std::hash()( wrapWidth ), std::hash()( keepIndentation ) ); } -template -TextLayout::Cache TextLayout::layout( const StringType& string, Font* font, +TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, const Uint32& characterSize, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, std::optional tabOffset, Uint32 textDrawHints, - TextDirection baseDirection ) { + TextDirection baseDirection, LineWrapMode wrapMode, + Uint32 wrapWidth, bool keepIndentation, + Float initialXOffset ) { static LRULayoutCache sLayoutCache; if ( !font || string.empty() ) { @@ -249,7 +254,7 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font, Uint64 hash = 0; if ( !Text::canSkipShaping( textDrawHints ) ) { hash = textLayoutHash( string, font, characterSize, style, tabWidth, outlineThickness, - tabOffset, baseDirection ); + tabOffset, baseDirection, wrapMode, wrapWidth, keepIndentation ); auto cacheHit = sLayoutCache.get( hash ); if ( cacheHit.has_value() ) @@ -261,7 +266,7 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font, Uint32 spaceGlyphIndex = 0; Float hspace = font->getGlyph( ' ', characterSize, bold, italic, outlineThickness ).advance; Float vspace = font->getLineSpacing( characterSize ); - Vector2f pen; + Vector2f pen{ initialXOffset, 0 }; Float maxWidth = 0; auto resultPtr = std::make_shared(); @@ -494,6 +499,14 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font, result.size = { maxWidth, std::ceil( pen.y ) }; result.hasMixedDirection = !!gdc.ltr + !!gdc.rtl + !!gdc.ttb + !!gdc.btt + !!gdc.other > 1; + 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 ); return resultPtr; } @@ -501,19 +514,12 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font, TextLayout::Cache TextLayout::layout( const String& string, Font* font, const Uint32& fontSize, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, std::optional tabOffset, - Uint32 textDrawHints, TextDirection baseDirection ) { - return TextLayout::layout( string, font, fontSize, style, tabWidth, outlineThickness, - tabOffset, textDrawHints, baseDirection ); -} - -TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, - const Uint32& fontSize, const Uint32& style, - const Uint32& tabWidth, const Float& outlineThickness, - std::optional tabOffset, Uint32 textDrawHints, - TextDirection baseDirection ) { - return TextLayout::layout( string, font, fontSize, style, tabWidth, - outlineThickness, tabOffset, textDrawHints, - baseDirection ); + Uint32 textDrawHints, TextDirection baseDirection, + LineWrapMode wrapMode, Uint32 wrapWidth, bool keepIndentation, + Float initialXOffset ) { + return TextLayout::layout( string.view(), font, fontSize, style, tabWidth, outlineThickness, + tabOffset, textDrawHints, baseDirection, wrapMode, wrapWidth, + keepIndentation, initialXOffset ); } std::vector TextLayout::getLinesWidth() const { @@ -524,4 +530,72 @@ std::vector TextLayout::getLinesWidth() const { return lw; } +void TextLayout::wrapLayout( const String::View& string, TextLayout& result, + LineWrapMode lineWrapMode, Float wrapWidth, Float vspace, + bool keepIndentation, Font* font, const Uint32& characterSize, + const Uint32& fontStyle, const Uint32& tabWidth, + 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; + Vector2f currentOffset( 0.f, 0.f ); + Sizef maxSize{ 0, vspace }; + + if ( keepIndentation && shapedGlyphCount ) { + sp.wrapInfo.paddingStart = LineWrap::computeOffsets( + string.substr( sp.shapedGlyphs[0].stringIndex ), font, characterSize, fontStyle, + outlineThickness, tabWidth, eemax( wrapWidth - hspace, hspace ) ); + } + + for ( std::size_t idx = 0; idx < shapedGlyphCount; idx++ ) { + ShapedGlyph& sg = sp.shapedGlyphs[idx]; + auto curChar = string[sg.stringIndex]; + + sg.position += currentOffset; + + if ( sg.position.x + sg.advance.x > wrapWidth ) { + std::size_t breakIndex = idx; + 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 ); + + // Break after the last space (start of the current word) + if ( performWordWrap ) + breakIndex = lastSpace + 1; + + if ( breakIndex > idx ) + breakIndex = idx; + + sp.wrapInfo.wraps.push_back( breakIndex ); + + ShapedGlyph& breakGlyph = sp.shapedGlyphs[breakIndex]; + Vector2f adjustment( -breakGlyph.position.x + sp.wrapInfo.paddingStart, vspace ); + + for ( std::size_t k = breakIndex; k <= idx; ++k ) + sp.shapedGlyphs[k].position += adjustment; + + maxSize.y = std::max( breakGlyph.position.y + vspace, maxSize.y ); + + currentOffset += adjustment; + lastSpace = std::string::npos; + } else if ( LineWrap::isWrapChar( curChar ) ) { + lastSpace = idx; + } + } + + sp.size = maxSize; + } + + result.size = result.paragraphs[result.paragraphs.size() - 1].size; +} + } // namespace EE::Graphics diff --git a/src/tests/unit_tests/fontrendering.cpp b/src/tests/unit_tests/fontrendering.cpp index 9a0cc89b1..23263757d 100644 --- a/src/tests/unit_tests/fontrendering.cpp +++ b/src/tests/unit_tests/fontrendering.cpp @@ -1,10 +1,12 @@ #include "utest.hpp" +#include #include #include #include #include #include +#include #include #include #include @@ -883,3 +885,83 @@ UTEST( FontRendering, TextWrap ) { runTest(); } } + +UTEST( FontRendering, TextLayoutWrap ) { + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + std::string loremIpsum; + FileSystem::fileGet( "assets/textfiles/lorem-ipsum.uext", loremIpsum ); + + const auto runTest = [&]() { + UIApplication app( + WindowSettings( 512, 555, "eepp - Text Layout Wrap", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + BatchRenderer* BR = GlobalBatchRenderer::instance(); + auto drawGlyph = [BR]( GlyphDrawable* gd, const Vector2f& position, const Color& color ) { + BR->quadsSetColor( color ); + BR->quadsSetTexCoord( gd->getSrcRect().Left, gd->getSrcRect().Top, + gd->getSrcRect().Left + gd->getSrcRect().Right, + gd->getSrcRect().Top + gd->getSrcRect().Bottom ); + BR->batchQuad( position.x + gd->getGlyphOffset().x, position.y + gd->getGlyphOffset().y, + gd->getDestSize().getWidth(), gd->getDestSize().getHeight() ); + }; + + app.getWindow()->setClearColor( RGB( 255, 255, 255 ) ); + app.getWindow()->clear(); + + Vector2f pos{ 5, 5 }; + Primitives p; + p.setColor( Color::Red ); + p.drawRectangle( Rectf( pos - 1.f, { 1, 546 } ) ); + p.drawRectangle( Rectf( pos - 1.f, { 501, 1 } ) ); + p.drawRectangle( Rectf( { pos.x - 1.f, 544 + pos.y }, { 502, 1 } ) ); + p.drawRectangle( Rectf( { 500 + pos.x, pos.y - 1.f }, { 1, 546 } ) ); + + FontTrueType* font = + static_cast( app.getUI()->getUIThemeManager()->getDefaultFont() ); + auto fontSize = 16; + Texture* fontTexture = font->getTexture( fontSize ); + BR->setBlendMode( BlendMode::Alpha() ); + BR->quadsBegin(); + BR->setTexture( fontTexture, fontTexture->getCoordinateType() ); + + String string( loremIpsum ); + + // Remove the emoji since it won't work in this context + if ( Font::isEmojiCodePoint( string[string.size() - 1] ) ) + string.pop_back(); + + auto layout = TextLayout::layout( string, font, fontSize, 0, 4, 0, {}, 0, + TextDirection::LeftToRight, LineWrapMode::Word, 500 ); + + for ( const auto& sp : layout->paragraphs ) { + for ( const auto& sg : sp.shapedGlyphs ) { + auto* gd = sg.font->getGlyphDrawableFromGlyphIndex( sg.glyphIndex, fontSize ); + if ( gd ) + drawGlyph( gd, pos + sg.position, Color::Black ); + } + } + + BR->draw(); + + compareImages( utest_state, utest_result, app.getWindow(), "eepp-text-layout-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(); + } +}