From 2008b57033b9ce0458e3626c6d0edc71bd8fe1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 10 Nov 2025 00:53:28 -0300 Subject: [PATCH] Added SheenBidi segmentation. --- .../fontrendering/eepp-textedit-bengali.webp | Bin 16802 -> 16820 bytes .../assets/textfiles/test-bengali.uext | 2 +- include/eepp/graphics/shapedglyph.hpp | 1 + include/eepp/graphics/text.hpp | 2 + include/eepp/graphics/textshaperun.hpp | 3 - projects/linux/ee.creator.user | 8 +- projects/linux/ee.includes | 2 + src/eepp/graphics/textlayouter.cpp | 252 ++++++++++++------ src/eepp/graphics/textshaperun.cpp | 19 +- src/tests/ui_perf_test/ui_perf_test.cpp | 19 +- src/tests/unit_tests/fontrendering.cpp | 1 + src/tools/ecode/appconfig.cpp | 2 + 12 files changed, 197 insertions(+), 114 deletions(-) diff --git a/bin/unit_tests/assets/fontrendering/eepp-textedit-bengali.webp b/bin/unit_tests/assets/fontrendering/eepp-textedit-bengali.webp index 63af90304dd507e1790013eee65138ce2db8a5bc..591dbacc5eec22f043b009babbc9a815c1b6840f 100644 GIT binary patch literal 16820 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 - + EnvironmentId @@ -126,7 +126,7 @@ {388e5431-b31b-42b3-b9ad-9002d279d75d} 10 0 - 19 + 28 ../../make/linux @@ -1600,7 +1600,7 @@ debug-all GenericProjectManager.GenericBuildConfiguration 0 - 19 + 28 0 @@ -1877,6 +1877,7 @@ ProjectExplorer.CustomExecutableRunConfiguration true + --text-shaper 0 false 1 @@ -16160,6 +16161,7 @@ ProjectExplorer.CustomExecutableRunConfiguration true + --text-shaper 0 false 1 diff --git a/projects/linux/ee.includes b/projects/linux/ee.includes index 1e9d82768..65df34d5b 100644 --- a/projects/linux/ee.includes +++ b/projects/linux/ee.includes @@ -15,5 +15,7 @@ ../../src/tools/ecode ../../src/thirdparty/mojoAL/ ../../src/thirdparty/pcre2/src +../../src/thirdparty/SheenBidi/Headers +../../src/thirdparty/SheenBidi/Headers/SheenBidi ../../src/modules/languages-syntax-highlighting/src /usr/include/freetype2/ diff --git a/src/eepp/graphics/textlayouter.cpp b/src/eepp/graphics/textlayouter.cpp index d4a52c462..4c47821fc 100644 --- a/src/eepp/graphics/textlayouter.cpp +++ b/src/eepp/graphics/textlayouter.cpp @@ -5,87 +5,162 @@ #include #ifdef EE_TEXT_SHAPER_ENABLED +#include #include #include #endif namespace EE::Graphics { -using LRULayoutCache = LRUCache<1024, Uint64, TextLayout>; +using LRULayoutCache = LRUCache<2048, Uint64, TextLayout>; #ifdef EE_TEXT_SHAPER_ENABLED -static bool -shapeAndRun( const String& string, FontTrueType* font, Uint32 characterSize, Uint32 style, - Float outlineThickness, - const std::function& cb ) { - TextShapeRun run( string.view(), font, characterSize, style, outlineThickness ); + +struct TextSegment { + std::size_t offset{}; + std::size_t length{}; + hb_script_t script{}; + hb_direction_t direction{}; +}; + +// Split string into segments with uniform text properties +static void segmentString( String::View input, + std::function cb ) { + const SBCodepointSequence codepointSequence{ + SBStringEncodingUTF32, static_cast( input.data() ), input.size() }; + auto* const scriptLocator = SBScriptLocatorCreate(); + auto* const algorithm = SBAlgorithmCreate( &codepointSequence ); + SBUInteger paragraphOffset = 0; + + while ( paragraphOffset < input.size() ) { + SBUInteger paragraphLength{}; + SBUInteger separatorLength{}; + SBAlgorithmGetParagraphBoundary( algorithm, paragraphOffset, + std::numeric_limits::max(), ¶graphLength, + &separatorLength ); + + auto* const paragraph = SBAlgorithmCreateParagraph( algorithm, paragraphOffset, + paragraphLength, SBLevelDefaultLTR ); + auto* const line = SBParagraphCreateLine( paragraph, paragraphOffset, paragraphLength ); + const auto runCount = SBLineGetRunCount( line ); + const auto* runArray = SBLineGetRunsPtr( line ); + + for ( SBUInteger i = 0; i < runCount; i++ ) { + // Odd levels are RTL, even levels are LTR + const auto direction = ( runArray[i].level % 2 ) ? HB_DIRECTION_RTL : HB_DIRECTION_LTR; + + const SBCodepointSequence codepointSubsequence{ + SBStringEncodingUTF32, + static_cast( input.data() + runArray[i].offset ), runArray[i].length }; + + SBScriptLocatorLoadCodepoints( scriptLocator, &codepointSubsequence ); + + while ( SBScriptLocatorMoveNext( scriptLocator ) ) { + const auto* agent = SBScriptLocatorGetAgent( scriptLocator ); + const auto script = + hb_script_from_iso15924_tag( SBScriptGetUnicodeTag( agent->script ) ); + + if ( !cb( TextSegment{ runArray[i].offset + agent->offset, agent->length, script, + direction } ) ) + break; + } + + SBScriptLocatorReset( scriptLocator ); + } + + SBLineRelease( line ); + SBParagraphRelease( paragraph ); + + paragraphOffset += paragraphLength; + } + + SBAlgorithmRelease( algorithm ); + SBScriptLocatorRelease( scriptLocator ); +} + +static inline bool isSimpleScript( hb_script_t script ) { + return script == HB_SCRIPT_LATIN || script == HB_SCRIPT_GREEK || script == HB_SCRIPT_CYRILLIC || + script == HB_SCRIPT_INVALID || script == HB_SCRIPT_COMMON; +} + +static bool shapeAndRun( const String& string, FontTrueType* font, Uint32 characterSize, + Uint32 style, Float outlineThickness, + const std::function& cb ) { + String::View input = string.view(); hb_buffer_t* hbBuffer = hb_buffer_create(); bool completeRun = true; - 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_guess_segment_properties( hbBuffer ); - hb_segment_properties_t props; - hb_buffer_get_segment_properties( hbBuffer, &props ); + segmentString( input, [&]( const TextSegment& segment ) { + TextShapeRun run( input.substr( segment.offset, segment.length ), font, characterSize, + style, outlineThickness ); - // We use our own kerning algo - static const hb_feature_t features[] = { - hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), 0, HB_FEATURE_GLOBAL_START, - HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'l', 'i', 'g', 'a' ), 0, HB_FEATURE_GLOBAL_START, - HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'c', 'l', 'i', 'g' ), 0, HB_FEATURE_GLOBAL_START, - HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'd', 'l', 'i', 'g' ), 0, HB_FEATURE_GLOBAL_START, - HB_FEATURE_GLOBAL_END }, - }; + 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() ); - // whitelist cross-platforms shapers only - static const char* shaper_list[] = { "ot", "graphite2", "fallback", nullptr }; + 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; - if ( !font || !font->hb() ) { - eeASSERT( font && font->hb() ); - completeRun = false; - break; + // 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 }; + + 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 ); - - // 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, run ) ) - run.next(); - else { - completeRun = false; - break; - } - } + return true; + } ); hb_buffer_destroy( hbBuffer ); return completeRun; } -// New helper function to identify scripts where our custom kerning is safe to apply. -static inline bool isSimpleScript( hb_script_t script ) { - // This list can be expanded, but covers the most common simple LTR scripts. - return script == HB_SCRIPT_LATIN || script == HB_SCRIPT_GREEK || script == HB_SCRIPT_CYRILLIC || - script == HB_SCRIPT_INVALID; -} - #endif template @@ -136,7 +211,8 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin shapeAndRun( string, rFont, characterSize, style, outlineThickness, [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t* glyphPos, Uint32 glyphCount, - const hb_segment_properties_t& props, TextShapeRun& run ) { + const hb_segment_properties_t& props, const TextSegment& segment, + TextShapeRun& run ) { FontTrueType* currentRunFont = run.font(); if ( !currentRunFont ) return true; @@ -146,7 +222,7 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin if ( isSimpleScript( props.script ) ) { for ( size_t i = 0; i < glyphCount; ++i ) { Uint32 cluster = glyphInfo[i].cluster; - String::StringBaseType ch = string[run.pos() + cluster]; + String::StringBaseType ch = string[segment.offset + run.pos() + cluster]; if ( ch == '\t' ) { Float advance = Text::tabAdvance( hspace, tabWidth, @@ -155,8 +231,9 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin ShapedGlyph sg; sg.font = currentRunFont; sg.glyphIndex = glyphInfo[i].codepoint; - sg.stringIndex = run.pos() + cluster; + sg.stringIndex = segment.offset + run.pos() + cluster; sg.position = pen; + sg.advance = { advance, 0 }; result.shapedGlyphs.emplace_back( std::move( sg ) ); pen.x += advance; @@ -176,10 +253,11 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin ShapedGlyph sg; sg.font = currentRunFont; sg.glyphIndex = glyphInfo[i].codepoint; - sg.stringIndex = run.pos() + glyphInfo[i].cluster; + sg.stringIndex = segment.offset + run.pos() + glyphInfo[i].cluster; float offsetX = glyphPos[i].x_offset / 64.f; float offsetY = glyphPos[i].y_offset / 64.f; + sg.advance = { offsetX, offsetY }; sg.position.x = pen.x + offsetX; sg.position.y = pen.y - offsetY; result.shapedGlyphs.emplace_back( std::move( sg ) ); @@ -190,7 +268,7 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin } else { for ( size_t i = 0; i < glyphCount; ++i ) { Uint32 cluster = glyphInfo[i].cluster; - String::StringBaseType ch = string[run.pos() + cluster]; + String::StringBaseType ch = string[segment.offset + run.pos() + cluster]; if ( ch == '\t' ) { Float advance = Text::tabAdvance( hspace, tabWidth, @@ -199,7 +277,8 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin ShapedGlyph sg; sg.font = currentRunFont; sg.glyphIndex = glyphInfo[i].codepoint; - sg.stringIndex = run.pos() + cluster; + sg.stringIndex = segment.offset + run.pos() + cluster; + sg.advance = { advance, 0 }; sg.position = pen; result.shapedGlyphs.emplace_back( std::move( sg ) ); @@ -211,20 +290,26 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin ShapedGlyph sg; sg.font = currentRunFont; sg.glyphIndex = glyphInfo[i].codepoint; - sg.stringIndex = run.pos() + glyphInfo[i].cluster; + sg.stringIndex = segment.offset + run.pos() + glyphInfo[i].cluster; float offsetX = glyphPos[i].x_offset / 64.f; float offsetY = glyphPos[i].y_offset / 64.f; - sg.position.x = pen.x + offsetX; - sg.position.y = pen.y - offsetY; + sg.advance = { offsetX, offsetY }; + sg.position.x = std::round( pen.x + offsetX ); + sg.position.y = std::round( pen.y - offsetY ); result.shapedGlyphs.emplace_back( std::move( sg ) ); - pen.x += glyphPos[i].x_advance / 64.f; + pen.x += Font::isEmojiCodePoint( ch ) + ? currentRunFont + ->getGlyphByIndex( glyphInfo[i].codepoint, characterSize, + bold, italic, outlineThickness ) + .advance + : glyphPos[i].x_advance / 64.f; pen.y += glyphPos[i].y_advance / 64.f; } } if ( run.runIsNewLine() ) { - result.linesWidth.push_back( pen.x ); - maxWidth = eemax( maxWidth, pen.x ); + result.linesWidth.push_back( std::ceil( pen.x ) ); + maxWidth = eemax( maxWidth, result.linesWidth[result.linesWidth.size() - 1] ); pen.x = 0; pen.y += vspace; } @@ -255,13 +340,16 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin prevChar = curChar; if ( curChar == '\t' ) { - pen.x += Text::tabAdvance( - hspace, tabWidth, - tabOffset ? ( tabOffset ? *tabOffset + pen.x : std::optional{} ) - : std::optional{} ); + ShapedGlyph sg; sg.stringIndex = i; + sg.advance = { Text::tabAdvance( hspace, tabWidth, + tabOffset ? ( tabOffset ? *tabOffset + pen.x + : std::optional{} ) + : std::optional{} ), + 0 }; sg.position = pen; + pen.x += sg.advance.x; result.shapedGlyphs.emplace_back( std::move( sg ) ); continue; } @@ -270,9 +358,11 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin sg.font = static_cast( font ); sg.glyphIndex = sg.font->getGlyphIndex( curChar ); sg.stringIndex = i; + sg.advance = { + font->getGlyph( curChar, characterSize, bold, italic, outlineThickness ).advance, + 0 }; sg.position = pen; - pen.x += - font->getGlyph( curChar, characterSize, bold, italic, outlineThickness ).advance; + pen.x += sg.advance.x; result.shapedGlyphs.emplace_back( std::move( sg ) ); } } @@ -281,9 +371,9 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin if ( string[string.size() - 1] != '\n' ) pen.y += vspace; - result.linesWidth.push_back( pen.x ); - maxWidth = eemax( maxWidth, pen.x ); - result.size = { maxWidth, pen.y }; + result.linesWidth.push_back( std::ceil( pen.x ) ); + maxWidth = eemax( maxWidth, result.linesWidth[result.linesWidth.size() - 1] ); + result.size = { maxWidth, std::ceil( pen.y ) }; sLayoutCache.put( hash, result ); return result; diff --git a/src/eepp/graphics/textshaperun.cpp b/src/eepp/graphics/textshaperun.cpp index bec303ea6..228556b97 100644 --- a/src/eepp/graphics/textshaperun.cpp +++ b/src/eepp/graphics/textshaperun.cpp @@ -50,11 +50,9 @@ void TextShapeRun::findNextEnd() { Font* lFont = mStartFont ? mStartFont : mFont; std::size_t len = mString.size(); std::size_t pos = 0; - hb_script_t curScript = HB_SCRIPT_UNKNOWN; for ( std::size_t idx = mIndex; idx < len; ++idx, ++pos ) { auto ch = mString[idx]; - hb_script_t script = hb_unicode_script( hb_unicode_funcs_get_default(), ch ); auto font = mFont ->getGlyph( ch, mCharacterSize, mStyle & Text::Bold, mStyle & Text::Italic, mOutlineThickness ) @@ -62,33 +60,22 @@ void TextShapeRun::findNextEnd() { mIsNewLine = ( ch == '\n' ); if ( idx == mIndex ) { - curScript = script; mStartFont = font; lFont = font; mCurFont = font; - if ( curScript == HB_SCRIPT_COMMON || curScript == HB_SCRIPT_INHERITED ) - curScript = HB_SCRIPT_LATIN; - mIsRTL = hb_script_get_horizontal_direction( curScript ) == HB_DIRECTION_RTL; } // Break run if: // - Newline // - Font changed - // - Script changed - hb_script_t effectiveScript = - ( script == HB_SCRIPT_COMMON || script == HB_SCRIPT_INHERITED ) ? (hb_script_t)curScript - : script; - - if ( mIsNewLine || ( lFont != nullptr && font != lFont ) || effectiveScript != curScript ) { + if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) { mLen = mIsNewLine ? pos + 1 : pos; mCurFont = lFont; - mIsRTL = hb_script_get_horizontal_direction( effectiveScript ) == HB_DIRECTION_RTL; return; } lFont = font; mCurFont = font; - curScript = effectiveScript; } mLen = len - mIndex; @@ -116,8 +103,4 @@ void TextShapeRun::findNextEnd() { #endif } -bool TextShapeRun::isRTL() const { - return mIsRTL; -} - } // namespace EE::Graphics diff --git a/src/tests/ui_perf_test/ui_perf_test.cpp b/src/tests/ui_perf_test/ui_perf_test.cpp index 2ec944681..36a53f3d8 100644 --- a/src/tests/ui_perf_test/ui_perf_test.cpp +++ b/src/tests/ui_perf_test/ui_perf_test.cpp @@ -153,34 +153,37 @@ void mainLoop() { EE_MAIN_FUNC int main( int, char*[] ) { { - Text::TextShaperEnabled = false; + Text::TextShaperEnabled = true; + Text::TextShaperOptimizations = false; UIApplication app( WindowSettings( 1024, 650, "eepp - TextEdit", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( {}, 1.5f ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); auto ll = UILinearLayout::NewVertical(); ll->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); - auto editor = UICodeEditor::New(); + auto editor = UITextEdit::New(); editor->setShowLineNumber( false ); editor->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); editor->setParent( ll ); - // editor->setFontSize( PixelDensity::dpToPx( 32 ) ); - /* FontManager::instance()->addFallbackFont( - FontTrueType::New( "arabic", "unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf" ) ); */ + editor->setFontSize( PixelDensity::dpToPx( 12 ) ); + // 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->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-arabic.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->getDocument().textInput( "اسمي..." ); // editor->getDocument().textInput( " হ্যাঁ " ); + // editor->getDocument().textInput( "I'm from...: আমি ... থেকে এসেছি।" ); + editor->setFont( app.getUI()->getUIThemeManager()->getDefaultFont() ); editor->on( Event::KeyUp, [&]( const Event* event ) { - if ( event->asKeyEvent()->getKeyCode() == KEY_F1 ){ + if ( event->asKeyEvent()->getKeyCode() == KEY_F1 ) { Text::TextShaperEnabled = !Text::TextShaperEnabled; app.getUI()->getRoot()->invalidateDraw(); } diff --git a/src/tests/unit_tests/fontrendering.cpp b/src/tests/unit_tests/fontrendering.cpp index eef6ca5c1..a0d406735 100644 --- a/src/tests/unit_tests/fontrendering.cpp +++ b/src/tests/unit_tests/fontrendering.cpp @@ -418,6 +418,7 @@ UTEST( FontRendering, textEditBengaliTest ) { FontManager::instance()->addFallbackFont( bengaliFont ); UTEST_PRINT_STEP( "Text Shaper enabled" ); auto* editor = UITextEdit::New(); + // editor->setFontSize( PixelDensity::dpToPx( 12 ) ); editor->setPixelsSize( app.getUI()->getPixelsSize() ); editor->loadFromFile( "assets/textfiles/test-bengali.uext" ); SceneManager::instance()->update(); diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 37e6f8cfd..39f0b65fc 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -143,6 +143,8 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, FontTrueType::fontHintingFromString( ini.getValue( "ui", "font_hinting", "full" ) ); ui.fontAntialiasing = FontTrueType::fontAntialiasingFromString( ini.getValue( "ui", "font_antialiasing", "grayscale" ) ); + Text::TextShaperEnabled |= ini.getValueB( "ui", "text_shaper", false ); + Text::TextShaperOptimizations |= ini.getValueB( "ui", "text_shaper_optimizations", true ); doc.trimTrailingWhitespaces = ini.getValueB( "document", "trim_trailing_whitespaces", false ); doc.forceNewLineAtEndOfFile = ini.getValueB( "document", "force_new_line_at_end_of_file", false );