diff --git a/bin/unit_tests/assets/fonts/NotoSansHebrew-Regular.ttf b/bin/unit_tests/assets/fonts/NotoSansHebrew-Regular.ttf new file mode 100644 index 000000000..b44a0db04 Binary files /dev/null and b/bin/unit_tests/assets/fonts/NotoSansHebrew-Regular.ttf differ diff --git a/bin/unit_tests/assets/fonts/NotoSansKR-Regular.ttf b/bin/unit_tests/assets/fonts/NotoSansKR-Regular.ttf new file mode 100644 index 000000000..dee35df71 Binary files /dev/null and b/bin/unit_tests/assets/fonts/NotoSansKR-Regular.ttf differ diff --git a/bin/unit_tests/assets/textfiles/test-arabic-mixed.uext b/bin/unit_tests/assets/textfiles/test-arabic-mixed.uext new file mode 100644 index 000000000..f52e27b09 --- /dev/null +++ b/bin/unit_tests/assets/textfiles/test-arabic-mixed.uext @@ -0,0 +1 @@ +الإنتاج سيتضاعف! الإيجار سيختفي! النصر للشعب! ⚒️🚩 \ No newline at end of file diff --git a/bin/unit_tests/assets/textfiles/test-mixed-text.uext b/bin/unit_tests/assets/textfiles/test-mixed-text.uext new file mode 100644 index 000000000..485486a8e --- /dev/null +++ b/bin/unit_tests/assets/textfiles/test-mixed-text.uext @@ -0,0 +1,7 @@ +Hello! +This is a test sentence mixing Latin script with Arabic (like مرحبا، يا صديقي! which means "Hello, my friend!") +and Bengali (আমার নাম রহস্যময়। which means "My name is mysterious."), plus some emojis 😊🚀🌟. +Now, let's embed RTL inside LTR: The word "English" followed by العربية (Arabic) and then back to "text". +In Bengali: বাংলা ভাষা is beautiful, and mixing with Arabic: বাংলা مع العربية. +Finally, numbers 123 in Arabic context: ١٢٣، and emojis in RTL: 😊 مرحبا 😊. +Punctuation test: (Hello? مرحبا!) [End: 😎]. diff --git a/include/eepp/graphics/textshaperun.hpp b/include/eepp/graphics/textshaperun.hpp index 43564c839..f89064f4b 100644 --- a/include/eepp/graphics/textshaperun.hpp +++ b/include/eepp/graphics/textshaperun.hpp @@ -12,7 +12,7 @@ class FontTrueType; class EE_API TextShapeRun { public: TextShapeRun( String::View str, FontTrueType* font, Uint32 characterSize, Uint32 style, - Float outlineThickness ); + Float outlineThickness, bool isRTL ); String::View curRun() const; @@ -29,16 +29,20 @@ class EE_API TextShapeRun { protected: void findNextEnd(); + inline bool checkRun( std::size_t idx, std::size_t pos ); + String::View mString; std::size_t mIndex{ 0 }; std::size_t mLen{ 0 }; Font* mFont{ nullptr }; + Font* mLFont{ nullptr }; Uint32 mCharacterSize; Uint32 mStyle; Float mOutlineThickness; Font* mCurFont{ nullptr }; Font* mStartFont{ nullptr }; - bool mIsNewLine{ false }; + bool mIsNewLine : 1 { false }; + bool mIsRTL : 1 { false }; }; } // namespace EE::Graphics diff --git a/src/eepp/graphics/textlayouter.cpp b/src/eepp/graphics/textlayouter.cpp index 4c47821fc..a7da13e47 100644 --- a/src/eepp/graphics/textlayouter.cpp +++ b/src/eepp/graphics/textlayouter.cpp @@ -24,8 +24,8 @@ struct TextSegment { }; // Split string into segments with uniform text properties -static void segmentString( String::View input, - std::function cb ) { +static void segmentString( String::View input, std::function cb, + const std::function& newParagraphCb ) { const SBCodepointSequence codepointSequence{ SBStringEncodingUTF32, static_cast( input.data() ), input.size() }; auto* const scriptLocator = SBScriptLocatorCreate(); @@ -39,6 +39,14 @@ static void segmentString( String::View input, std::numeric_limits::max(), ¶graphLength, &separatorLength ); + // If the paragraph contains characters besides the separator, + // split the separator off into its own paragraph in the next iteration + // We do this to ensure line breaks are inserted into segments last + // after all character runs on the same line have already been inserted + // This allows us to draw our segments in left-to-right top-to-bottom order + if ( separatorLength < paragraphLength ) + paragraphLength -= separatorLength; + auto* const paragraph = SBAlgorithmCreateParagraph( algorithm, paragraphOffset, paragraphLength, SBLevelDefaultLTR ); auto* const line = SBParagraphCreateLine( paragraph, paragraphOffset, paragraphLength ); @@ -72,6 +80,8 @@ static void segmentString( String::View input, SBParagraphRelease( paragraph ); paragraphOffset += paragraphLength; + + newParagraphCb(); } SBAlgorithmRelease( algorithm ); @@ -87,75 +97,83 @@ static bool shapeAndRun( const String& string, FontTrueType* font, Uint32 charac Uint32 style, Float outlineThickness, const std::function& cb ) { + const TextSegment&, TextShapeRun& )>& cb, + const std::function& newParagraphCb ) { String::View input = string.view(); hb_buffer_t* hbBuffer = hb_buffer_create(); bool completeRun = true; - segmentString( input, [&]( const TextSegment& segment ) { - TextShapeRun run( input.substr( segment.offset, segment.length ), font, characterSize, - style, outlineThickness ); + segmentString( + input, + [&]( const TextSegment& segment ) { + TextShapeRun run( input.substr( segment.offset, segment.length ), font, characterSize, + style, outlineThickness, segment.direction == HB_DIRECTION_RTL ); - while ( run.hasNext() ) { - FontTrueType* font = run.font(); - if ( font == nullptr ) { // empty line - run.next(); - continue; - } - String::View curRun( run.curRun() ); - font->setCurrentSize( characterSize ); - hb_buffer_reset( hbBuffer ); - hb_buffer_set_cluster_level( hbBuffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS ); - hb_buffer_add_utf32( hbBuffer, (Uint32*)curRun.data(), curRun.size(), 0, - curRun.size() ); + while ( run.hasNext() ) { + FontTrueType* font = run.font(); + if ( font == nullptr ) { // empty line + run.next(); + continue; + } + String::View curRun( run.curRun() ); + font->setCurrentSize( characterSize ); + hb_buffer_reset( hbBuffer ); + hb_buffer_set_cluster_level( hbBuffer, + HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS ); + hb_buffer_add_utf32( hbBuffer, (Uint32*)curRun.data(), curRun.size(), 0, + curRun.size() ); - hb_buffer_set_direction( hbBuffer, segment.direction ); - hb_buffer_set_script( hbBuffer, segment.script ); - hb_buffer_guess_segment_properties( hbBuffer ); - hb_segment_properties_t props; - hb_buffer_get_segment_properties( hbBuffer, &props ); - std::uint32_t featuresEnabled = !isSimpleScript( segment.script ) ? 1 : 0; + hb_buffer_set_direction( hbBuffer, segment.direction ); + hb_buffer_set_script( hbBuffer, segment.script ); + hb_buffer_guess_segment_properties( hbBuffer ); + hb_segment_properties_t props; + hb_buffer_get_segment_properties( hbBuffer, &props ); + std::uint32_t featuresEnabled = !isSimpleScript( segment.script ) ? 1 : 0; - // We use our own kerning algo - const hb_feature_t features[] = { - hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), featuresEnabled, - HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'l', 'i', 'g', 'a' ), featuresEnabled, - HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'c', 'l', 'i', 'g' ), featuresEnabled, - HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'd', 'l', 'i', 'g' ), featuresEnabled, - HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, - }; + // We use our own kerning algo + const hb_feature_t features[] = { + hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), featuresEnabled, + HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + hb_feature_t{ HB_TAG( 'l', 'i', 'g', 'a' ), featuresEnabled, + HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + hb_feature_t{ HB_TAG( 'c', 'l', 'i', 'g' ), featuresEnabled, + HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + hb_feature_t{ HB_TAG( 'd', 'l', 'i', 'g' ), featuresEnabled, + HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + }; - // whitelist cross-platforms shapers only - static const char* shaper_list[] = { "ot", "graphite2", "fallback", nullptr }; + // whitelist cross-platforms shapers only + static const char* shaper_list[] = { "ot", "graphite2", "fallback", nullptr }; - if ( !font || !font->hb() ) { - eeASSERT( font && font->hb() ); - completeRun = false; - break; + if ( !font || !font->hb() ) { + eeASSERT( font && font->hb() ); + completeRun = false; + break; + } + + hb_shape_full( static_cast( font->hb() ), hbBuffer, features, + eeARRAY_SIZE( features ), shaper_list ); + + // from the shaped text we get the glyphs and positions + unsigned int glyphCount; + hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos( hbBuffer, &glyphCount ); + hb_glyph_position_t* glyphPos = + hb_buffer_get_glyph_positions( hbBuffer, &glyphCount ); + + if ( cb( glyphInfo, glyphPos, glyphCount, props, segment, run ) ) + run.next(); + else { + completeRun = false; + return false; + break; + } } - hb_shape_full( static_cast( font->hb() ), hbBuffer, features, - eeARRAY_SIZE( features ), shaper_list ); + return true; + }, + [] { - // from the shaped text we get the glyphs and positions - unsigned int glyphCount; - hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos( hbBuffer, &glyphCount ); - hb_glyph_position_t* glyphPos = hb_buffer_get_glyph_positions( hbBuffer, &glyphCount ); - - if ( cb( glyphInfo, glyphPos, glyphCount, props, segment, run ) ) - run.next(); - else { - completeRun = false; - return false; - break; - } - } - - return true; - } ); + } ); hb_buffer_destroy( hbBuffer ); return completeRun; @@ -314,6 +332,12 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin pen.y += vspace; } return true; + }, + [&] { + result.linesWidth.push_back( std::ceil( pen.x ) ); + maxWidth = eemax( maxWidth, result.linesWidth[result.linesWidth.size() - 1] ); + pen.x = 0; + pen.y += vspace; } ); } else #endif diff --git a/src/eepp/graphics/textshaperun.cpp b/src/eepp/graphics/textshaperun.cpp index 228556b97..bb58b824b 100644 --- a/src/eepp/graphics/textshaperun.cpp +++ b/src/eepp/graphics/textshaperun.cpp @@ -10,30 +10,43 @@ namespace EE::Graphics { TextShapeRun::TextShapeRun( String::View str, FontTrueType* font, Uint32 characterSize, - Uint32 style, Float outlineThickness ) : + Uint32 style, Float outlineThickness, bool isRTL ) : mString( str ), mFont( font ), mCharacterSize( characterSize ), mStyle( style ), mOutlineThickness( outlineThickness ), - mCurFont( mFont ) { + mCurFont( mFont ), + mIsRTL( isRTL ) { + if ( mIsRTL ) + mIndex = mString.size(); findNextEnd(); } String::View TextShapeRun::curRun() const { + if ( mIsRTL ) + return mString.substr( mIndex - mLen, mIsNewLine ? mLen - 1 : mLen ); return mString.substr( mIndex, mIsNewLine ? mLen - 1 : mLen ); } bool TextShapeRun::hasNext() const { + if ( mIsRTL ) + return mIndex > 0; return mIndex < mString.size(); } std::size_t TextShapeRun::pos() const { + if ( mIsRTL ) + return mIndex - mLen; return mIndex; } void TextShapeRun::next() { - mIndex += mLen; + if ( mIsRTL ) { + mIndex -= mLen; + } else { + mIndex += mLen; + } findNextEnd(); } @@ -46,60 +59,122 @@ FontTrueType* TextShapeRun::font() { } void TextShapeRun::findNextEnd() { + if ( !hasNext() ) { + mLen = 0; + return; + } + #ifdef EE_TEXT_SHAPER_ENABLED - Font* lFont = mStartFont ? mStartFont : mFont; - std::size_t len = mString.size(); - std::size_t pos = 0; + if ( !mIsRTL ) { + Font* lFont = mStartFont ? mStartFont : mFont; + std::size_t len = mString.size(); + std::size_t pos = 0; - for ( std::size_t idx = mIndex; idx < len; ++idx, ++pos ) { - auto ch = mString[idx]; - auto font = mFont - ->getGlyph( ch, mCharacterSize, mStyle & Text::Bold, mStyle & Text::Italic, - mOutlineThickness ) - .font; - mIsNewLine = ( ch == '\n' ); + for ( std::size_t idx = mIndex; idx < len; ++idx, ++pos ) { + auto ch = mString[idx]; + auto font = mFont + ->getGlyph( ch, mCharacterSize, mStyle & Text::Bold, + mStyle & Text::Italic, mOutlineThickness ) + .font; + mIsNewLine = ( ch == '\n' ); + + if ( idx == mIndex ) { + mStartFont = font; + lFont = font; + mCurFont = font; + } + + // Break run if: + // - Newline + // - Font changed + if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) { + mLen = mIsNewLine ? pos + 1 : pos; + mCurFont = lFont; + return; + } - if ( idx == mIndex ) { - mStartFont = font; lFont = font; mCurFont = font; } - // Break run if: - // - Newline - // - Font changed - if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) { - mLen = mIsNewLine ? pos + 1 : pos; - mCurFont = lFont; - return; + mLen = len - mIndex; + } else { + Font* lFont = mStartFont ? mStartFont : mFont; + std::size_t pos = 0; + + for ( ; pos < mIndex; ++pos ) { + std::size_t idx = mIndex - 1 - pos; + auto ch = mString[idx]; + auto font = mFont + ->getGlyph( ch, mCharacterSize, mStyle & Text::Bold, + mStyle & Text::Italic, mOutlineThickness ) + .font; + mIsNewLine = ( ch == '\n' ); + + if ( pos == 0 ) { + mStartFont = font; + lFont = font; + mCurFont = font; + } + + if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) { + mLen = mIsNewLine ? pos + 1 : pos; + mCurFont = lFont; + return; + } + + lFont = font; + mCurFont = font; } - lFont = font; - mCurFont = font; + mLen = mIndex; } - - mLen = len - mIndex; #else - Font* lFont = mStartFont; - std::size_t len = mString.size(); - std::size_t idx; - std::size_t pos = 0; - for ( idx = mIndex; idx < len; idx++, pos++ ) { - Font* font = mFont - ->getGlyph( mString[idx], mCharacterSize, mStyle & Text::Bold, - mStyle & Text::Italic, mOutlineThickness ) - .font; - mIsNewLine = mString[idx] == '\n'; - if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) { - mCurFont = lFont; - mStartFont = font; - mLen = mIsNewLine ? pos + 1 : pos; - return; + if ( !mIsRTL ) { + Font* lFont = mStartFont; + std::size_t len = mString.size(); + std::size_t pos = 0; + for ( std::size_t idx = mIndex; idx < len; idx++, pos++ ) { + Font* font = mFont + ->getGlyph( mString[idx], mCharacterSize, mStyle & Text::Bold, + mStyle & Text::Italic, mOutlineThickness ) + .font; + mIsNewLine = mString[idx] == '\n'; + if ( pos == 0 ) + lFont = font; + if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) { + mCurFont = lFont; + mStartFont = font; + mLen = mIsNewLine ? pos + 1 : pos; + return; + } + lFont = font; + mCurFont = font; } - lFont = font; - mCurFont = font; + mLen = pos; + } else { + Font* lFont = mStartFont; + std::size_t pos = 0; + for ( ; pos < mIndex; ++pos ) { + std::size_t idx = mIndex - 1 - pos; + Font* font = mFont + ->getGlyph( mString[idx], mCharacterSize, mStyle & Text::Bold, + mStyle & Text::Italic, mOutlineThickness ) + .font; + mIsNewLine = mString[idx] == '\n'; + if ( pos == 0 ) + lFont = font; + if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) { + mCurFont = lFont; + mStartFont = font; + mLen = mIsNewLine ? pos + 1 : pos; + return; + } + lFont = font; + mCurFont = font; + } + mLen = mIndex; } - mLen = idx; #endif } diff --git a/src/tests/ui_perf_test/ui_perf_test.cpp b/src/tests/ui_perf_test/ui_perf_test.cpp index 36a53f3d8..8d3ac38ef 100644 --- a/src/tests/ui_perf_test/ui_perf_test.cpp +++ b/src/tests/ui_perf_test/ui_perf_test.cpp @@ -166,17 +166,18 @@ EE_MAIN_FUNC int main( int, char*[] ) { editor->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); editor->setParent( ll ); editor->setFontSize( PixelDensity::dpToPx( 12 ) ); - // FontManager::instance()->addFallbackFont( - // FontTrueType::New( "arabic", "unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf" ) ); + FontManager::instance()->addFallbackFont( + 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->setFont( FontManager::instance()->getByName( "monospace" ) ); + 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" ); - editor->loadFromFile( "unit_tests/assets/textfiles/test-bengali.uext" ); + // editor->loadFromFile( "unit_tests/assets/textfiles/test-bengali.uext" ); // editor->loadFromFile( "unit_tests/assets/textfiles/test-flags.uext" ); // editor->loadFromFile( "unit_tests/assets/textformat/english.utf8.lf.nobom.txt" ); + editor->loadFromFile( "unit_tests/assets/textfiles/test-arabic-mixed.uext" ); // editor->getDocument().textInput( "اسمي..." ); // editor->getDocument().textInput( " হ্যাঁ " ); // editor->getDocument().textInput( "I'm from...: আমি ... থেকে এসেছি।" );