diff --git a/.ecode/project_build.json b/.ecode/project_build.json index 71d8165c1..8550b92ec 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -204,7 +204,7 @@ }, "run": [ { - "args": "", + "args": "--text-shaper", "command": "${project_root}/bin/ecode-debug", "name": "ecode-debug", "working_dir": "${project_root}/bin" diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-bold.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-bold.webp new file mode 100644 index 000000000..1e93aaaaf Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-bold.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-italic.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-italic.webp new file mode 100644 index 000000000..b4035d13a Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-italic.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-outline.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-outline.webp new file mode 100644 index 000000000..7a34066b2 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-outline.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-regular-center.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-regular-center.webp new file mode 100644 index 000000000..db01a1e07 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-regular-center.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-regular-right.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-regular-right.webp new file mode 100644 index 000000000..bdadabd7b Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-regular-right.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-regular.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-regular.webp new file mode 100644 index 000000000..e83f9611d Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-regular.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-shadow.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-shadow.webp new file mode 100644 index 000000000..03d4c8733 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-shadow.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough-center.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough-center.webp new file mode 100644 index 000000000..8fcb28db3 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough-center.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough-right.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough-right.webp new file mode 100644 index 000000000..591270051 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough-right.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough.webp new file mode 100644 index 000000000..4252f5846 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-strikethrough.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-underline-center.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-underline-center.webp new file mode 100644 index 000000000..3827dadd7 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-underline-center.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-underline-right.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-underline-right.webp new file mode 100644 index 000000000..b6f13bc99 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-underline-right.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-text-style-underline.webp b/bin/unit_tests/assets/fontrendering/eepp-text-style-underline.webp new file mode 100644 index 000000000..c158b5cd8 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-text-style-underline.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-textedit-bengali.webp b/bin/unit_tests/assets/fontrendering/eepp-textedit-bengali.webp index 765597994..36cdff38a 100644 Binary files a/bin/unit_tests/assets/fontrendering/eepp-textedit-bengali.webp and b/bin/unit_tests/assets/fontrendering/eepp-textedit-bengali.webp differ diff --git a/bin/unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf b/bin/unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf new file mode 100644 index 000000000..ec013bea2 Binary files /dev/null and b/bin/unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf differ diff --git a/bin/unit_tests/assets/fonts/NotoSansBengali-Regular.ttf b/bin/unit_tests/assets/fonts/NotoSansBengali-Regular.ttf new file mode 100644 index 000000000..03436c3ab Binary files /dev/null and b/bin/unit_tests/assets/fonts/NotoSansBengali-Regular.ttf differ diff --git a/bin/unit_tests/assets/fonts/NotoSerifBengali-Regular.ttf b/bin/unit_tests/assets/fonts/NotoSerifBengali-Regular.ttf deleted file mode 100644 index a9351b8d6..000000000 Binary files a/bin/unit_tests/assets/fonts/NotoSerifBengali-Regular.ttf and /dev/null differ diff --git a/bin/unit_tests/assets/textfiles/test-arabic-long-phrase.uext b/bin/unit_tests/assets/textfiles/test-arabic-long-phrase.uext new file mode 100644 index 000000000..646cae5e6 --- /dev/null +++ b/bin/unit_tests/assets/textfiles/test-arabic-long-phrase.uext @@ -0,0 +1 @@ +اسکم شاخ و دم نداره همین که کاربر شبکه خودت. کسی که رو شبکه تو فی داده سالها زحمت کشیده رو نادیده میگیری و به کاربر یه شبکه دیگه توکن سنگین میدی میشه اسکم علنی. باید کاری باهاش کنیم که مثل استارک به غلط کردن بیافته اره تو endgame هستی اخر اسکمرایی diff --git a/bin/unit_tests/assets/textfiles/test-arabic-simple.uext b/bin/unit_tests/assets/textfiles/test-arabic-simple.uext new file mode 100644 index 000000000..0518d8fce --- /dev/null +++ b/bin/unit_tests/assets/textfiles/test-arabic-simple.uext @@ -0,0 +1 @@ +مَرْحَبًا بِالْعَالَم diff --git a/bin/unit_tests/assets/textfiles/test-arabic.uext b/bin/unit_tests/assets/textfiles/test-arabic.uext index 646cae5e6..f148e5c67 100644 --- a/bin/unit_tests/assets/textfiles/test-arabic.uext +++ b/bin/unit_tests/assets/textfiles/test-arabic.uext @@ -1 +1,24 @@ -اسکم شاخ و دم نداره همین که کاربر شبکه خودت. کسی که رو شبکه تو فی داده سالها زحمت کشیده رو نادیده میگیری و به کاربر یه شبکه دیگه توکن سنگین میدی میشه اسکم علنی. باید کاری باهاش کنیم که مثل استارک به غلط کردن بیافته اره تو endgame هستی اخر اسکمرایی +Hello: مرحبا +Good morning: صباح الخير +Good night: تصبح على خير +Thank you: شكراً +You're welcome: عفواً +Yes / No: نعم / لا +Please: من فضلك +Excuse me / Sorry: عفواً / آسف +How are you?: كيف حالك؟ +I'm fine. And you?: أنا بخير. وأنت؟ +What's your name?: ما اسمك؟ +My name is...: اسمي... +Nice to meet you: تشرفت بلقائك +Where are you from?: من أين أنت؟ +I'm from...: أنا من... +Do you speak English?: هل تتحدث الإنجليزية؟ +I don't understand: لا أفهم +Please speak more slowly: من فضلك تكلم ببطء أكثر +Please write it down: من فضلك اكتبه +How much is this?: كم سعره؟ +Where is the bathroom?: أين الحمام؟ +Help!: المساعدة! +Stop!: توقف! +Call the police!: اتصل بالشرطة! diff --git a/bin/unit_tests/assets/textfiles/test-bengali.uext b/bin/unit_tests/assets/textfiles/test-bengali.uext index 5f6394e9c..aa6f39ae3 100644 --- a/bin/unit_tests/assets/textfiles/test-bengali.uext +++ b/bin/unit_tests/assets/textfiles/test-bengali.uext @@ -1,24 +1,24 @@ -Hello: হ্যালো / নমস্কার -Good morning: সুপ্রভাত -Good night: শুভ রাত্রি -Thank you: ধন্যবাদ -You're welcome: আপনি স্বাগত জানাই -Yes / No: হ্যাঁ / না -Please: অনুগ্রহ করে -Excuse me / Sorry: মাফ করবেন / দুঃখিত -How are you?: আপনি কেমন আছেন? -I'm fine. And you?: আমি ভালো আছি। এবং আপনি? -What's your name?: আপনার নাম কি? -My name is...: আমার নাম... -Nice to meet you: আপনার সাথে দেখা করে খুশি -Where are you from?: আপনি কোথা থেকে এসেছেন? -I'm from...: আমি ... থেকে এসেছি। -Do you speak English?: আপনি কি ইংরেজি বলতে পারেন? -I don't understand: আমি বুঝতে পারছি না। -Please speak more slowly: অনুগ্রহ করে ধীরে বলুন। -Please write it down: অনুগ্রহ করে এটি লিখে দিন। -How much is this?: এটার দাম কত? -Where is the bathroom?: বাথরুম কোথায়? -Help!: বাঁচাও! -Stop!: থামুন! -Call the police!: পুলিশ ডাকুন! +Hello: হ্যালো / নমস্কার +Good morning: সুপ্রভাত +Good night: শুভ রাত্রি +Thank you: ধন্যবাদ +You're welcome: আপনি স্বাগত জানাই +Yes / No: হ্যাঁ / না +Please: অনুগ্রহ করে +Excuse me / Sorry: মাফ করবেন / দুঃখিত +How are you?: আপনি কেমন আছেন? +I'm fine. And you?: আমি ভালো আছি। এবং আপনি? +What's your name?: আপনার নাম কি? +My name is...: আমার নাম... +Nice to meet you: আপনার সাথে দেখা করে খুশি +Where are you from?: আপনি কোথা থেকে এসেছেন? +I'm from...: আমি ... থেকে এসেছি। +Do you speak English?: আপনি কি ইংরেজি বলতে পারেন? +I don't understand: আমি বুঝতে পারছি না। +Please speak more slowly: অনুগ্রহ করে ধীরে বলুন। +Please write it down: অনুগ্রহ করে এটি লিখে দিন। +How much is this?: এটার দাম কত? +Where is the bathroom?: বাথরুম কোথায়? +Help!: বাঁচাও! +Stop!: থামুন! +Call the police!: পুলিশ ডাকুন! diff --git a/bin/unit_tests/assets/fontrendering/tabs_test.txt b/bin/unit_tests/assets/textfiles/test-tabs.txt similarity index 100% rename from bin/unit_tests/assets/fontrendering/tabs_test.txt rename to bin/unit_tests/assets/textfiles/test-tabs.txt diff --git a/include/eepp/graphics/fonttruetype.hpp b/include/eepp/graphics/fonttruetype.hpp index 315be30c6..353a3f299 100644 --- a/include/eepp/graphics/fonttruetype.hpp +++ b/include/eepp/graphics/fonttruetype.hpp @@ -154,6 +154,7 @@ class EE_API FontTrueType : public Font { protected: friend class Text; + friend class TextLayouter; explicit FontTrueType( const std::string& FontName ); diff --git a/include/eepp/graphics/text.hpp b/include/eepp/graphics/text.hpp index a6b6e3c9f..ba646407f 100644 --- a/include/eepp/graphics/text.hpp +++ b/include/eepp/graphics/text.hpp @@ -10,6 +10,8 @@ namespace EE { namespace Graphics { +class FontTrueType; + enum class CharacterAlignment : Uint32 { Left = 0, Center = 1, Right = 2 }; struct WhitespaceDisplayConfig { @@ -20,9 +22,76 @@ struct WhitespaceDisplayConfig { std::optional tabOffset; }; +struct ShapedGlyph { + FontTrueType* font{ nullptr }; + Uint32 glyphIndex{ 0 }; + Uint32 stringIndex{ 0 }; + Vector2f position; +}; + +struct TextLayout { + std::vector shapedGlyphs; + std::vector linesWidth; + Sizef size; +}; + +class EE_API TextLayouter { + public: + static TextLayout layout( const String& string, Font* font, const Uint32& fontSize, + const Uint32& style, const Uint32& tabWidth = 4, + const Float& outlineThickness = 0.f, + std::optional tabOffset = {}, Uint32 textDrawHints = 0 ); + + static TextLayout 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 ); + + protected: + template + static TextLayout 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 ); +}; + +// helper class that divides the string into lines and font runs. +class EE_API TextShapeRun { + public: + TextShapeRun( String::View str, FontTrueType* font, Uint32 characterSize, Uint32 style, + Float outlineThickness ); + + String::View curRun() const; + + bool hasNext() const; + + std::size_t pos() const; + + void next(); + + bool runIsNewLine() const; + + FontTrueType* font(); + + protected: + void findNextEnd(); + + String::View mString; + std::size_t mIndex{ 0 }; + std::size_t mLen{ 0 }; + Font* mFont{ nullptr }; + Uint32 mCharacterSize; + Uint32 mStyle; + Float mOutlineThickness; + Font* mCurFont{ nullptr }; + Font* mStartFont{ nullptr }; + bool mIsNewLine{ false }; +}; + class EE_API Text { public: static bool TextShaperEnabled; + static bool TextShaperOptimizations; static Uint32 GlobalInvalidationId; enum Style { @@ -98,14 +167,16 @@ class EE_API Text { const Uint32& fontSize, const String& string, const Uint32& style, const Uint32& tabWidth = 4, const Float& outlineThickness = 0.f, - std::optional tabOffset = {} ); + std::optional tabOffset = {}, + Uint32 textDrawHints = 0 ); static Vector2f findCharacterPos( std::size_t index, Font* font, const Uint32& fontSize, const String& string, const Uint32& style, const Uint32& tabWidth = 4, const Float& outlineThickness = 0.f, std::optional tabOffset = {}, - bool allowNewLine = true ); + bool allowNewLine = true, + Uint32 textDrawHints = 0 ); static std::size_t findLastCharPosWithinLength( Font* font, const Uint32& fontSize, const String& string, Float maxWidth, diff --git a/include/eepp/math/rect.hpp b/include/eepp/math/rect.hpp index ab212942a..ee63d2716 100644 --- a/include/eepp/math/rect.hpp +++ b/include/eepp/math/rect.hpp @@ -79,6 +79,8 @@ template class tRECT { T getHeight() const; + void normalize(); + tRECT ceil() const; tRECT floor() const; @@ -265,6 +267,11 @@ template tSize tRECT::getSize() const { return tSize( eeabs( Right - Left ), eeabs( Bottom - Top ) ); } +template void tRECT::normalize() { + if ( Left > Right ) std::swap( Left, Right ); + if ( Top > Bottom ) std::swap( Top, Bottom ); +} + template T tRECT::getWidth() const { return eeabs( Right - Left ); } diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index bae125d51..d5f69f031 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -540,11 +540,13 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { size_t characterWidth( const String& str ) const; - Float getTextWidth( const String& text, std::optional tabOffset = {} ) const; + Float getTextWidth( const String& text, std::optional tabOffset = {}, + Uint32 textHints = 0 ) const; size_t characterWidth( const String::View& str ) const; - Float getTextWidth( const String::View& text, std::optional tabOffset ) const; + Float getTextWidth( const String::View& text, std::optional tabOffset, + Uint32 textHints = 0 ) const; Float getLineHeight() const; @@ -1131,13 +1133,13 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { template Float getTextWidth( const StringType& text, bool fromMonospaceLine, - std::optional tabOffset ) const; + std::optional tabOffset, Uint32 textHints = 0 ) const; - Float getTextWidth( const String& text, bool fromMonospaceLine, - std::optional tabOffset ) const; + Float getTextWidth( const String& text, bool fromMonospaceLine, std::optional tabOffset, + Uint32 textHints = 0 ) const; Float getTextWidth( const String::View& text, bool fromMonospaceLine, - std::optional tabOffset ) const; + std::optional tabOffset, Uint32 textHints = 0 ) const; void updateIMELocation(); diff --git a/src/eepp/graphics/fonttruetype.cpp b/src/eepp/graphics/fonttruetype.cpp index d07745ae1..ee5dda144 100644 --- a/src/eepp/graphics/fonttruetype.cpp +++ b/src/eepp/graphics/fonttruetype.cpp @@ -1290,11 +1290,17 @@ bool FontTrueType::setCurrentSize( unsigned int characterSize ) const { } } else if ( characterSize != currentSize && ( result = FT_Set_Pixel_Sizes( face, 0, it->second ) ) == FT_Err_Ok ) { +#ifdef EE_TEXT_SHAPER_ENABLED + hb_ft_font_changed( (hb_font_t*)mHBFont ); +#endif return true; } } } +#ifdef EE_TEXT_SHAPER_ENABLED + hb_ft_font_changed( (hb_font_t*)mHBFont ); +#endif return result == FT_Err_Ok; } else { return true; diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index 63d9d8e96..20141fffe 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -19,83 +19,14 @@ namespace EE { namespace Graphics { -namespace { - -// helper class that divides the string into lines and font runs. -class TextShapeRun { - public: - TextShapeRun( String::View str, FontTrueType* font, Uint32 characterSize, Uint32 style, - Float outlineThickness ) : - mString( str ), - mFont( font ), - mCharacterSize( characterSize ), - mStyle( style ), - mOutlineThickness( outlineThickness ), - mCurFont( mFont ) { - findNextEnd(); - } - - std::size_t curRunStart() { return mIndex; } - - String::View curRun() const { return mString.substr( mIndex, mIsNewLine ? mLen - 1 : mLen ); } - - bool hasNext() const { return mIndex < mString.size(); } - - std::size_t pos() const { return mIndex; } - - void next() { - mIndex += mLen; - findNextEnd(); - } - - bool runIsNewLine() const { return mIsNewLine; } - - FontTrueType* font() { return static_cast( mCurFont ); } - - private: - void findNextEnd() { - 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; - } - lFont = font; - mCurFont = font; - } - mLen = idx; - } - - String::View mString; - std::size_t mIndex{ 0 }; - std::size_t mLen{ 0 }; - Font* mFont{ nullptr }; - Uint32 mCharacterSize; - Uint32 mStyle; - Float mOutlineThickness; - Font* mCurFont{ nullptr }; - Font* mStartFont{ nullptr }; - bool mIsNewLine{ false }; -}; - #ifdef EE_TEXT_SHAPER_ENABLED static bool shapeAndRun( const String& string, FontTrueType* font, Uint32 characterSize, Uint32 style, Float outlineThickness, const std::function& cb ) { - hb_buffer_t* hbBuffer = hb_buffer_create(); TextShapeRun run( string.view(), font, characterSize, style, outlineThickness ); + hb_buffer_t* hbBuffer = hb_buffer_create(); bool completeRun = true; while ( run.hasNext() ) { @@ -126,7 +57,7 @@ shapeAndRun( const String& string, FontTrueType* font, Uint32 characterSize, Uin }; // whitelist cross-platforms shapers only - static const char* shaper_list[] = { "graphite2", "ot", "fallback", nullptr }; + static const char* shaper_list[] = { "ot", "graphite2", "fallback", nullptr }; if ( !font || !font->hb() ) { eeASSERT( font && font->hb() ); @@ -153,18 +84,317 @@ shapeAndRun( const String& string, FontTrueType* font, Uint32 characterSize, Uin hb_buffer_destroy( hbBuffer ); return completeRun; } - -static bool -shapeAndRun( const String& string, const FontStyleConfig& config, - const std::function& cb ) { - return shapeAndRun( string, static_cast( config.Font ), config.CharacterSize, - config.Style, config.OutlineThickness, cb ); -} - #endif -} // namespace +// 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; +} + +static inline bool canSkipShaping( Uint32 textDrawHints ) { + return Text::TextShaperOptimizations && ( textDrawHints & TextHints::AllAscii ) != 0; +} + +template +TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uint32& characterSize, + const Uint32& style, const Uint32& tabWidth, + const Float& outlineThickness, std::optional tabOffset, + Uint32 textDrawHints ) { + TextLayout result; + + if ( !font || string.empty() ) { + result.size = { 0.f, font ? (Float)font->getFontHeight( characterSize ) : 0.f }; + return result; + } + + bool bold = ( style & Text::Bold ) != 0; + bool italic = ( style & Text::Italic ) != 0; + Float hspace = font->getGlyph( ' ', characterSize, bold, italic, outlineThickness ).advance; + Float vspace = font->getLineSpacing( characterSize ); + Vector2f pen; + Float maxWidth = 0; + +#ifdef EE_TEXT_SHAPER_ENABLED + if ( Text::TextShaperEnabled && font->getType() == FontType::TTF && + !canSkipShaping( textDrawHints ) ) { + FontTrueType* rFont = static_cast( font ); + 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 ) { + FontTrueType* currentRunFont = run.font(); + if ( !currentRunFont ) + return true; + result.shapedGlyphs.reserve( result.shapedGlyphs.size() + glyphCount ); + Uint32 prevGlyphIndex = 0; + + if ( isSimpleScript( props.script ) ) { + for ( size_t i = 0; i < glyphCount; ++i ) { + Uint32 cluster = glyphInfo[i].cluster; + String::StringBaseType ch = string[run.pos() + cluster]; + + if ( ch == '\t' ) { + Float advance = Text::tabAdvance( hspace, tabWidth, + tabOffset ? pen.x + *tabOffset + : std::optional{} ); + ShapedGlyph sg; + sg.font = currentRunFont; + sg.glyphIndex = glyphInfo[i].codepoint; + sg.stringIndex = run.pos() + cluster; + sg.position = pen; + result.shapedGlyphs.emplace_back( std::move( sg ) ); + + pen.x += advance; + prevGlyphIndex = 0; // Reset kerning after a tab + continue; + } + + Glyph currentGlyph = currentRunFont->getGlyphByIndex( + glyphInfo[i].codepoint, characterSize, bold, italic, outlineThickness ); + + if ( ch != '\n' && ch != '\r' ) { + pen.x += currentRunFont->getKerningFromGlyphIndex( + prevGlyphIndex, glyphInfo[i].codepoint, characterSize, bold, italic, + outlineThickness ); + } + + ShapedGlyph sg; + sg.font = currentRunFont; + sg.glyphIndex = glyphInfo[i].codepoint; + sg.stringIndex = 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; + result.shapedGlyphs.emplace_back( std::move( sg ) ); + + pen.x += currentGlyph.advance; + prevGlyphIndex = glyphInfo[i].codepoint; + } + } else { + for ( size_t i = 0; i < glyphCount; ++i ) { + Uint32 cluster = glyphInfo[i].cluster; + String::StringBaseType ch = string[run.pos() + cluster]; + + if ( ch == '\t' ) { + Float advance = Text::tabAdvance( hspace, tabWidth, + tabOffset ? pen.x + *tabOffset + : std::optional{} ); + ShapedGlyph sg; + sg.font = currentRunFont; + sg.glyphIndex = glyphInfo[i].codepoint; + sg.stringIndex = run.pos() + cluster; + sg.position = pen; + result.shapedGlyphs.emplace_back( std::move( sg ) ); + + pen.x += advance; + prevGlyphIndex = 0; // Reset kerning after a tab + continue; + } + + ShapedGlyph sg; + sg.font = currentRunFont; + sg.glyphIndex = glyphInfo[i].codepoint; + sg.stringIndex = 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; + result.shapedGlyphs.emplace_back( std::move( sg ) ); + pen.x += 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 ); + pen.x = 0; + pen.y += vspace; + } + return true; + } ); + } else +#endif + { + // Fallback for non-TrueType fonts or when shaper is disabled + Uint32 prevChar = 0; + for ( size_t i = 0; i < string.size(); ++i ) { + Uint32 curChar = string[i]; + if ( curChar == '\n' ) { + result.linesWidth.push_back( pen.x ); + maxWidth = eemax( maxWidth, pen.x ); + pen.x = 0; + pen.y += vspace; + prevChar = 0; + continue; + } + if ( curChar == '\r' ) { + prevChar = 0; + continue; + } + + pen.x += font->getKerning( prevChar, curChar, characterSize, bold, italic, + outlineThickness ); + 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.position = pen; + result.shapedGlyphs.emplace_back( std::move( sg ) ); + continue; + } + + ShapedGlyph sg; + sg.font = static_cast( font ); + sg.glyphIndex = sg.font->getGlyphIndex( curChar ); + sg.stringIndex = i; + sg.position = pen; + pen.x += + font->getGlyph( curChar, characterSize, bold, italic, outlineThickness ).advance; + result.shapedGlyphs.emplace_back( std::move( sg ) ); + } + } + + // pen.y doesn't have the last line height counted unless the last run ended with a new line + 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 }; + + return result; +} + +TextLayout TextLayouter::layout( const String& string, Font* font, const Uint32& fontSize, + const Uint32& style, const Uint32& tabWidth, + const Float& outlineThickness, std::optional tabOffset, + Uint32 textDrawHints ) { + return TextLayouter::layout( string, font, fontSize, style, tabWidth, outlineThickness, + tabOffset, textDrawHints ); +} + +TextLayout TextLayouter::layout( const String::View& string, Font* font, const Uint32& fontSize, + const Uint32& style, const Uint32& tabWidth, + const Float& outlineThickness, std::optional tabOffset, + Uint32 textDrawHints ) { + return TextLayouter::layout( string, font, fontSize, style, tabWidth, + outlineThickness, tabOffset, textDrawHints ); +} + +TextShapeRun::TextShapeRun( String::View str, FontTrueType* font, Uint32 characterSize, + Uint32 style, Float outlineThickness ) : + mString( str ), + mFont( font ), + mCharacterSize( characterSize ), + mStyle( style ), + mOutlineThickness( outlineThickness ), + mCurFont( mFont ) { + findNextEnd(); +} + +String::View TextShapeRun::curRun() const { + return mString.substr( mIndex, mIsNewLine ? mLen - 1 : mLen ); +} + +bool TextShapeRun::hasNext() const { + return mIndex < mString.size(); +} + +std::size_t TextShapeRun::pos() const { + return mIndex; +} + +void TextShapeRun::next() { + mIndex += mLen; + findNextEnd(); +} + +bool TextShapeRun::runIsNewLine() const { + return mIsNewLine; +} + +FontTrueType* TextShapeRun::font() { + return static_cast( mCurFont ); +} + +void TextShapeRun::findNextEnd() { +#ifdef EE_TEXT_SHAPER_ENABLED + 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 ) + .font; + 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; + } + + // 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 ) { + mLen = mIsNewLine ? pos + 1 : pos; + mCurFont = lFont; + return; + } + + lFont = font; + mCurFont = font; + curScript = effectiveScript; + } + + 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; + } + lFont = font; + mCurFont = font; + } + mLen = idx; +#endif +} Float Text::tabAdvance( Float hspace, Uint32 tabWidth, std::optional tabOffset ) { Float advance = hspace * tabWidth; @@ -179,6 +409,7 @@ Float Text::tabAdvance( Float hspace, Uint32 tabWidth, std::optional tabO } bool Text::TextShaperEnabled = false; +bool Text::TextShaperOptimizations = true; Uint32 Text::GlobalInvalidationId = 0; std::string Text::styleFlagToString( const Uint32& flags ) { @@ -449,135 +680,82 @@ Sizef Text::draw( const StringType& string, const Vector2f& pos, Font* font, Flo BR->setTexture( fontTexture, fontTexture->getCoordinateType() ); #ifdef EE_TEXT_SHAPER_ENABLED - if ( TextShaperEnabled && font->getType() == FontType::TTF ) { + if ( TextShaperEnabled && font->getType() == FontType::TTF && + !canSkipShaping( textDrawHints ) ) { FontTrueType* rFont = static_cast( font ); - shapeAndRun( - string, rFont, fontSize, style, outlineThickness, - [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t*, Uint32 glyphCount, - const hb_segment_properties_t&, TextShapeRun& run ) { - FontTrueType* font = run.font(); - Uint32 prevGlyphIndex = 0; - Uint32 cluster = 0; - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - cluster = curGlyph.cluster; - ch = string[run.curRunStart() + cluster]; - if ( ch == '\t' ) { - Float advance = tabAdvance( hspace, tabWidth, - tabOffset ? cpos.x - pos.x + *tabOffset - : std::optional{} ); - if ( whitespaceDisplayConfig.tabDisplayCharacter ) { - switch ( whitespaceDisplayConfig.tabAlign ) { - case CharacterAlignment::Center: - tabAlign = - ( advance - tabGlyph->getPixelsSize().getWidth() ) * 0.5f; - break; - case CharacterAlignment::Right: - tabAlign = advance - tabGlyph->getPixelsSize().getWidth(); - break; - case CharacterAlignment::Left: - break; - } - } + auto layout = TextLayouter::layout( string, rFont, fontSize, style, tabWidth, + outlineThickness, tabOffset, textDrawHints ); - if ( tabGlyph ) { - drawGlyph( BR, tabGlyph, { cpos.x + tabAlign, cpos.y }, - whitespaceDisplayConfig.color, isItalic ); - } - width += advance; - cpos.x += advance; - } else { - if ( style & Text::Shadow ) { - auto* gds = font->getGlyphDrawableFromGlyphIndex( - curGlyph.codepoint, fontSize, isBold, isItalic, outlineThickness, - rFont->getPage( fontSize ) ); - if ( gds ) - drawGlyph( BR, gds, cpos, shadowColor, isItalic ); - } + for ( const ShapedGlyph& sg : layout.shapedGlyphs ) { + auto ch = string[sg.stringIndex]; + auto gpos( ( sg.position + pos ).trunc() ); - if ( outlineThickness != 0.f ) { - auto* gdo = font->getGlyphDrawableFromGlyphIndex( - curGlyph.codepoint, fontSize, isBold, isItalic, outlineThickness, - rFont->getPage( fontSize ) ); - if ( gdo ) - drawGlyph( BR, gdo, cpos, outlineColor, isItalic ); - } + if ( ch == '\t' ) { + if ( whitespaceDisplayConfig.tabDisplayCharacter ) { + Float advance = tabAdvance( hspace, tabWidth, + tabOffset ? gpos.x - pos.x + *tabOffset + : std::optional{} ); - auto* gd = font->getGlyphDrawableFromGlyphIndex( - curGlyph.codepoint, fontSize, isBold, isItalic, 0, - rFont->getPage( fontSize ) ); - if ( gd ) { - if ( !isMonospace ) { - kerning = font->getKerningFromGlyphIndex( - prevGlyphIndex, curGlyph.codepoint, fontSize, isBold, isItalic, - outlineThickness ); - cpos.x += kerning; - width += kerning; - } - - drawGlyph( BR, gd, cpos, - fallbacksToColorEmoji && Font::isEmojiCodePoint( ch ) - ? Color::White - : fontColor, - isItalic ); - - if ( ch == ' ' && whitespaceDisplayConfig.spaceDisplayCharacter ) { - if ( spaceGlyph == nullptr ) { - spaceGlyph = font->getGlyphDrawable( - whitespaceDisplayConfig.spaceDisplayCharacter, fontSize ); - } - drawGlyph( BR, spaceGlyph, cpos, whitespaceDisplayConfig.color, - isItalic ); - } - - Float advance = font->isColorEmojiFont() && ' ' != ch - ? gd->getPixelsSize().getWidth() - : gd->getAdvance(); - cpos.x += advance; - width += advance; - } + switch ( whitespaceDisplayConfig.tabAlign ) { + case CharacterAlignment::Center: + tabAlign = ( advance - tabGlyph->getPixelsSize().getWidth() ) * 0.5f; + break; + case CharacterAlignment::Right: + tabAlign = advance - tabGlyph->getPixelsSize().getWidth(); + break; + case CharacterAlignment::Left: + break; } - prevGlyphIndex = curGlyph.codepoint; + if ( tabGlyph ) { + drawGlyph( BR, tabGlyph, { gpos.x + tabAlign, gpos.y }, + whitespaceDisplayConfig.color, isItalic ); + } } + continue; + } - if ( run.runIsNewLine() ) { - if ( style & Text::Underlined ) { - _drawUnderline( font, fontSize, fontColor, cpos, style, BR, - outlineThickness, pos, width, shadowColor, shadowOffset, - outlineColor ); + if ( ch == ' ' ) { + if ( whitespaceDisplayConfig.spaceDisplayCharacter ) { + if ( spaceGlyph == nullptr ) { + spaceGlyph = font->getGlyphDrawable( + whitespaceDisplayConfig.spaceDisplayCharacter, fontSize ); } - if ( style & Text::StrikeThrough ) { - _drawStrikeThrough( font, fontSize, fontColor, cpos, style, BR, - outlineThickness, pos, width, shadowColor, shadowOffset, - outlineColor ); - } - size.x = eemax( width, size.x ); - width = 0; - cpos.x = pos.x; - cpos.y += height; - if ( cluster != ssize - 1 ) - size.y += height; + drawGlyph( BR, spaceGlyph, gpos, whitespaceDisplayConfig.color, isItalic ); } - return true; - } ); + continue; + } - if ( ( style & Text::Underlined ) && width != 0 ) { - _drawUnderline( font, fontSize, fontColor, cpos, style, BR, outlineThickness, pos, - width, shadowColor, shadowOffset, outlineColor ); + if ( style & Text::Shadow ) { + auto* gds = sg.font->getGlyphDrawableFromGlyphIndex( + sg.glyphIndex, fontSize, isBold, isItalic, outlineThickness, + rFont->getPage( fontSize ) ); + if ( gds ) + drawGlyph( BR, gds, gpos, shadowColor, isItalic ); + } + + if ( outlineThickness != 0.f ) { + auto* gdo = sg.font->getGlyphDrawableFromGlyphIndex( + sg.glyphIndex, fontSize, isBold, isItalic, outlineThickness, + rFont->getPage( fontSize ) ); + if ( gdo ) + drawGlyph( BR, gdo, gpos, outlineColor, isItalic ); + } + + auto* gd = sg.font->getGlyphDrawableFromGlyphIndex( + sg.glyphIndex, fontSize, isBold, isItalic, 0, rFont->getPage( fontSize ) ); + if ( gd ) { + drawGlyph( BR, gd, gpos, + fallbacksToColorEmoji && Font::isEmojiCodePoint( ch ) ? Color::White + : fontColor, + isItalic ); + } } - if ( ( style & Text::StrikeThrough ) && width != 0 ) { - _drawStrikeThrough( font, fontSize, fontColor, cpos, style, BR, outlineThickness, pos, - width, shadowColor, shadowOffset, outlineColor ); - } - - size.x = eemax( width, size.x ); - BR->drawOpt(); - return size; + return layout.size; } #endif @@ -742,7 +920,7 @@ bool Text::wrapText( Font* font, const Uint32& fontSize, StringType& string, con tWordWidth += fCharWidth; x += fCharWidth; - if ( *tChar != '\r' ) { + if ( *tChar != '\n' && *tChar != '\r' ) { tWordWidth += font->getKerning( prevChar, *tChar, fontSize, bold, italic, outlineThickness ); prevChar = *tChar; @@ -1112,40 +1290,11 @@ Float Text::getTextWidth( Font* font, const Uint32& fontSize, const StringType& } #ifdef EE_TEXT_SHAPER_ENABLED - if ( TextShaperEnabled && font->getType() == FontType::TTF ) { - FontTrueType* rFont = static_cast( font ); - shapeAndRun( - string, rFont, fontSize, style, outlineThickness, - [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t*, Uint32 glyphCount, - const hb_segment_properties_t&, TextShapeRun& run ) { - FontTrueType* font = run.font(); - Uint32 prevGlyphIndex = 0; - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - auto curChar = string[run.curRunStart() + curGlyph.cluster]; - if ( curChar == '\t' ) { - width += tabAdvance( hspace, tabWidth, - tabOffset ? *tabOffset + width : tabOffset ); - } else { - Glyph glyph = - font->getGlyphByIndex( curGlyph.codepoint, fontSize, bold, italic, - outlineThickness, rFont->getPage( fontSize ) ); - - width += rFont->getKerningFromGlyphIndex( prevGlyphIndex, - curGlyph.codepoint, fontSize, - bold, italic, outlineThickness ); - - width += font->isColorEmojiFont() && ' ' != curChar ? glyph.size.getWidth() - : glyph.advance; - } - maxWidth = eemax( maxWidth, width ); - prevGlyphIndex = curGlyph.codepoint; - } - if ( run.runIsNewLine() ) - width = 0; - return true; - } ); - return maxWidth; + if ( TextShaperEnabled && font->getType() == FontType::TTF && + !canSkipShaping( textDrawHints ) ) { + return TextLayouter::layout( string, static_cast( font ), fontSize, style, + tabWidth, outlineThickness, tabOffset, textDrawHints ) + .size.getWidth(); } #endif @@ -1156,6 +1305,8 @@ Float Text::getTextWidth( Font* font, const Uint32& fontSize, const StringType& width += tabAdvance( hspace, tabWidth, tabOffset ? *tabOffset + width : tabOffset ); } else if ( codepoint == '\n' ) { width = 0; + prevChar = 0; + continue; } else if ( codepoint != '\r' ) { width += font->getKerning( prevChar, codepoint, fontSize, bold, italic, outlineThickness ); @@ -1184,50 +1335,18 @@ Text::findLastCharPosWithinLength( Font* font, const Uint32& fontSize, const Str #ifdef EE_TEXT_SHAPER_ENABLED if ( TextShaperEnabled && font->getType() == FontType::TTF ) { - FontTrueType* rFont = static_cast( font ); - std::size_t it = 0; - std::size_t pos = 0; - bool completeRun = shapeAndRun( - string, rFont, fontSize, style, outlineThickness, - [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t*, Uint32 glyphCount, - const hb_segment_properties_t&, TextShapeRun& run ) { - FontTrueType* font = run.font(); - Uint32 prevGlyphIndex = 0; - - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - auto curChar = string[run.curRunStart() + curGlyph.cluster]; - - if ( curChar == '\t' ) { - width += tabAdvance( hspace, tabWidth, - tabOffset ? *tabOffset + width : tabOffset ); - } else { - Glyph glyph = - font->getGlyphByIndex( curGlyph.codepoint, fontSize, bold, italic, - outlineThickness, rFont->getPage( fontSize ) ); - - width += rFont->getKerningFromGlyphIndex( prevGlyphIndex, - curGlyph.codepoint, fontSize, - bold, italic, outlineThickness ); - - width += font->isColorEmojiFont() && ' ' != curChar ? glyph.size.getWidth() - : glyph.advance; - } - - if ( width > maxWidth ) { - pos = it > 0 ? it - 1 : 0; - return false; - } - - prevGlyphIndex = curGlyph.codepoint; - it++; - } - if ( run.runIsNewLine() ) - width = 0; - return true; - } ); - return completeRun ? ( width <= maxWidth ? string.size() : eemax( (size_t)0, it - 1 ) ) - : pos; + auto layout = + TextLayouter::layout( string, static_cast( font ), fontSize, style, + tabWidth, outlineThickness, tabOffset /* , textDrawHints */ ); + size_t lastStringIndex = 0; + for ( const ShapedGlyph& sg : layout.shapedGlyphs ) { + Glyph metrics = + sg.font->getGlyphByIndex( sg.glyphIndex, fontSize, bold, italic, outlineThickness ); + if ( sg.position.x + metrics.advance > maxWidth ) + return lastStringIndex; + lastStringIndex = sg.stringIndex; + } + return string.size(); } #endif @@ -1253,7 +1372,7 @@ Text::findLastCharPosWithinLength( Font* font, const Uint32& fontSize, const Str Vector2f Text::findCharacterPos( std::size_t index, Font* font, const Uint32& fontSize, const String& string, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, std::optional tabOffset, - bool allowNewLine ) { + bool allowNewLine, Uint32 textDrawHints ) { // Make sure that we have a valid font if ( !font ) return Vector2f(); @@ -1273,59 +1392,42 @@ Vector2f Text::findCharacterPos( std::size_t index, Font* font, const Uint32& fo Vector2f position; #ifdef EE_TEXT_SHAPER_ENABLED - if ( TextShaperEnabled && font->getType() == FontType::TTF ) { - FontTrueType* rFont = static_cast( font ); - std::size_t curPos = 0; - shapeAndRun( string, rFont, fontSize, style, outlineThickness, - [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t*, Uint32 glyphCount, - const hb_segment_properties_t&, TextShapeRun& run ) { - curPos = run.pos(); + if ( TextShaperEnabled && font->getType() == FontType::TTF && + !canSkipShaping( textDrawHints ) ) { + auto layout = TextLayouter::layout( string, font, fontSize, style, tabWidth, + outlineThickness, tabOffset ); + Uint32 maxStringIndex = 0; + Uint32 closestDist = std::numeric_limits::max(); - if ( index == curPos ) - return false; + const ShapedGlyph* msg = nullptr; + const ShapedGlyph* csg = nullptr; - FontTrueType* font = run.font(); - Uint32 prevGlyphIndex = 0; + for ( const ShapedGlyph& sg : layout.shapedGlyphs ) { + if ( sg.stringIndex >= maxStringIndex ) { + maxStringIndex = std::max( maxStringIndex, sg.stringIndex ); + msg = &sg; + } - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - curPos = run.pos() + curGlyph.cluster; + auto dist = std::abs( (Int64)index - (Int64)sg.stringIndex ); + if ( dist < closestDist ) { + closestDist = dist; + csg = &sg; + } - if ( curPos >= index ) - return false; + if ( sg.stringIndex == index ) + return sg.position.trunc(); + } - auto curChar = string[curPos]; + if ( !layout.shapedGlyphs.empty() && index >= maxStringIndex + 1 && msg ) { + Glyph metrics = msg->font->getGlyphByIndex( msg->glyphIndex, fontSize, bold, italic, + outlineThickness ); + return ( msg->position + Vector2f{ metrics.advance, 0 } ).trunc(); + } - if ( curChar == '\t' ) { - position.x += - tabAdvance( hspace, tabWidth, - tabOffset ? *tabOffset + position.x : tabOffset ); - } else { - Glyph glyph = font->getGlyphByIndex( - curGlyph.codepoint, fontSize, bold, italic, outlineThickness, - rFont->getPage( fontSize ) ); + if ( csg && closestDist != std::numeric_limits::max() ) { + return csg->position.trunc(); + } - position.x += rFont->getKerningFromGlyphIndex( - prevGlyphIndex, curGlyph.codepoint, fontSize, bold, italic, - outlineThickness ); - - position.x += font->isColorEmojiFont() && ' ' != curChar - ? glyph.size.getWidth() - : glyph.advance; - } - - prevGlyphIndex = curGlyph.codepoint; - if ( curPos >= index ) - return false; - } - - if ( run.runIsNewLine() && allowNewLine ) { - position.x = 0; - position.y = vspace; - } - - return true; - } ); return position; } #endif @@ -1375,7 +1477,7 @@ Vector2f Text::findCharacterPos( std::size_t index, Font* font, const Uint32& fo Int32 Text::findCharacterFromPos( const Vector2i& pos, bool returnNearest, Font* font, const Uint32& fontSize, const String& string, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, - std::optional tabOffset ) { + std::optional tabOffset, Uint32 textDrawHints ) { if ( NULL == font ) return 0; @@ -1395,79 +1497,80 @@ Int32 Text::findCharacterFromPos( const Vector2i& pos, bool returnNearest, Font* font->getGlyph( L' ', fontSize, bold, italic, outlineThickness ).advance ); #ifdef EE_TEXT_SHAPER_ENABLED - if ( TextShaperEnabled && font->getType() == FontType::TTF ) { - FontTrueType* rFont = static_cast( font ); - bool completeRun = shapeAndRun( - string, rFont, fontSize, style, outlineThickness, - [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t*, Uint32 glyphCount, - const hb_segment_properties_t&, TextShapeRun& run ) { - FontTrueType* font = run.font(); - Uint32 prevGlyphIndex = 0; + if ( TextShaperEnabled && font->getType() == FontType::TTF && + !canSkipShaping( textDrawHints ) ) { + auto layout = TextLayouter::layout( string, font, fontSize, style, tabWidth, + outlineThickness, tabOffset ); + auto sgs = layout.shapedGlyphs.size(); + if ( sgs == 0 ) + return 0; - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - auto curChar = string[run.curRunStart() + curGlyph.cluster]; - lWidth = width; + for ( size_t i = 0; i < sgs; i++ ) { + const ShapedGlyph& sg = layout.shapedGlyphs[i]; - if ( curChar == '\t' ) { - width += tabAdvance( hspace, tabWidth, - tabOffset ? *tabOffset + width : tabOffset ); + Glyph metrics = + sg.font->getGlyphByIndex( sg.glyphIndex, fontSize, bold, italic, outlineThickness ); + + // Define the boundaries of the character's clickable cell + Float charLeft = sg.position.x; + Float charTop = sg.position.y; + Float charBottom = charTop + vspace; + Float charRight; + + bool isLastOnLine = + ( i + 1 == sgs || layout.shapedGlyphs[i + 1].position.y != sg.position.y ); + + if ( !isLastOnLine ) { + // The cell extends to the beginning of the next glyph + charRight = layout.shapedGlyphs[i + 1].position.x; + } else { + // For the last glyph on a line, the cell is its advance width + charRight = charLeft + metrics.advance; + } + + // In complex scripts, visual order might not guarantee x increases (e.g. negative + // kerning). Ensure right > left for hit-testing, otherwise the cell has no width or + // negative width. + if ( charRight <= charLeft ) + charRight = charLeft + metrics.advance; + + // --- Direct Hit Test --- + // Check if the point is within the vertical bounds of the current line + if ( fpos.y >= charTop && fpos.y <= charBottom ) { + auto findNextInsertionIndex = [&]() -> Int32 { + // Return insertion point after this glyph. Find the next distinct stringIndex. + for ( size_t j = i + 1; j < sgs; ++j ) { + if ( layout.shapedGlyphs[j].stringIndex > sg.stringIndex ) + return layout.shapedGlyphs[j].stringIndex; + } + return tSize; // Reached the end + }; + + // Case 1: Point is within the horizontal bounds of this glyph's cell + if ( fpos.x >= charLeft && fpos.x < charRight ) { + Float midPoint = charLeft + ( charRight - charLeft ) * 0.5f; + if ( fpos.x < midPoint ) { + return sg.stringIndex; } else { - Glyph glyph = - font->getGlyphByIndex( curGlyph.codepoint, fontSize, bold, italic, - outlineThickness, rFont->getPage( fontSize ) ); - - width += rFont->getKerningFromGlyphIndex( prevGlyphIndex, - curGlyph.codepoint, fontSize, - bold, italic, outlineThickness ); - - width += font->isColorEmojiFont() && ' ' != curChar ? glyph.size.getWidth() - : glyph.advance; - } - - if ( pos.x <= width && pos.x >= lWidth && pos.y <= height && - pos.y >= lHeight ) { - if ( run.pos() + curGlyph.cluster + 1 <= tSize ) { - Int32 tcurDist = eeabs( pos.x - lWidth ); - Int32 nextDist = eeabs( pos.x - width ); - if ( nextDist < tcurDist ) { - nearest = run.pos() + curGlyph.cluster + 1; - return false; - } - } - nearest = run.pos() + curGlyph.cluster; - return false; - } - - if ( returnNearest ) { - curDist = eeabs( - fpos.distance( Vector2f( width - ( width - lWidth ) * 0.5f, - height - ( height - lHeight ) * 0.5f ) ) ); - if ( curDist < minDist ) { - nearest = run.pos() + curGlyph.cluster; - minDist = curDist; - } - } - - prevGlyphIndex = curGlyph.codepoint; - } - - if ( run.runIsNewLine() ) { - lWidth = 0; - width = 0; - lHeight = height; - height += vspace; - if ( pos.x > width && pos.y <= lHeight ) { - nearest = run.pos() + glyphInfo[glyphCount - 1].cluster + 1; - return false; + return findNextInsertionIndex(); } } + // Case 2: Point is to the right of the last glyph on the line + else if ( isLastOnLine && fpos.x >= charRight ) { + return findNextInsertionIndex(); + } + } - return true; - } ); - - if ( completeRun && pos.x >= width ) - return tSize; + // --- Nearest Character Test --- + if ( returnNearest ) { + Vector2f cellCenter( ( charLeft + charRight ) * 0.5f, charTop + vspace * 0.5f ); + Int32 dist = static_cast( fpos.distance( cellCenter ) ); + if ( dist < minDist ) { + minDist = dist; + nearest = sg.stringIndex; // Store the index of the character itself + } + } + } return nearest; } #endif @@ -1577,52 +1680,11 @@ void Text::updateWidthCache() { #ifdef EE_TEXT_SHAPER_ENABLED if ( TextShaperEnabled && mFontStyleConfig.Font->getType() == FontType::TTF ) { - FontTrueType* rFont = static_cast( mFontStyleConfig.Font ); - shapeAndRun( mString, mFontStyleConfig, - [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t*, Uint32 glyphCount, - const hb_segment_properties_t&, TextShapeRun& run ) { - FontTrueType* font = run.font(); - Uint32 prevGlyphIndex = 0; - - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - auto curChar = mString[run.curRunStart() + curGlyph.cluster]; - - if ( curChar == '\t' ) { - width += tabAdvance( hspace, mTabWidth, - mTabStops ? width : std::optional{} ); - } else { - Glyph glyph = font->getGlyphByIndex( - curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, - italic, mFontStyleConfig.OutlineThickness, - rFont->getPage( mFontStyleConfig.CharacterSize ) ); - - width += rFont->getKerningFromGlyphIndex( - prevGlyphIndex, curGlyph.codepoint, - mFontStyleConfig.CharacterSize, bold, italic, - mFontStyleConfig.OutlineThickness ); - - width += font->isColorEmojiFont() && ' ' != curChar - ? glyph.size.getWidth() - : glyph.advance; - } - - maxWidth = eemax( maxWidth, width ); - prevGlyphIndex = curGlyph.codepoint; - } - - if ( run.runIsNewLine() ) { - mLinesWidth.push_back( width ); - width = 0; - } - - return true; - } ); - - if ( !mString.empty() && mString[mString.size() - 1] != '\n' ) - mLinesWidth.push_back( width ); - - mCachedWidth = maxWidth; + auto layout = TextLayouter::layout( mString, mFontStyleConfig.Font, + mFontStyleConfig.CharacterSize, mFontStyleConfig.Style, + mTabWidth, mFontStyleConfig.OutlineThickness ); + mLinesWidth = std::move( layout.linesWidth ); + mCachedWidth = layout.size.getWidth(); return; } #endif @@ -1635,23 +1697,29 @@ void Text::updateWidthCache() { auto glyph = mFontStyleConfig.Font->getGlyph( codepoint, mFontStyleConfig.CharacterSize, bold, italic, mFontStyleConfig.OutlineThickness ); - if ( codepoint != '\r' && codepoint != '\t' ) { - width += mFontStyleConfig.Font->getKerning( prevChar, codepoint, - mFontStyleConfig.CharacterSize, bold, - italic, mFontStyleConfig.OutlineThickness ); + + if ( codepoint != '\t' && codepoint != '\n' ) { + auto kerning = mFontStyleConfig.Font->getKerning( + prevChar, codepoint, mFontStyleConfig.CharacterSize, bold, italic, + mFontStyleConfig.OutlineThickness ); + width += kerning; + width += glyph.advance; } else if ( codepoint == '\t' ) { width += tabAdvance( hspace, mTabWidth, mTabStops ? width : std::optional{} ); + } else if ( codepoint == '\r' ) { + prevChar = 0; + continue; } if ( codepoint == '\n' ) { - mLinesWidth.push_back( width - glyph.advance ); + mLinesWidth.push_back( width ); width = 0; + prevChar = 0; + } else { + maxWidth = eemax( maxWidth, width ); + prevChar = codepoint; } - - if ( width > maxWidth ) - maxWidth = width; - prevChar = codepoint; } if ( !mString.empty() && mString[mString.size() - 1] != '\n' ) @@ -1925,162 +1993,146 @@ void Text::ensureGeometryUpdate() { Float centerDiffX = 0; unsigned int line = 0; - switch ( Font::getHorizontalAlign( mAlign ) ) { - case TEXT_ALIGN_CENTER: - centerDiffX = line < mLinesWidth.size() - ? (Float)( (Int32)( ( mCachedWidth - mLinesWidth[line] ) * 0.5f ) ) - : 0.f; - line++; - break; - case TEXT_ALIGN_RIGHT: - centerDiffX = line < mLinesWidth.size() ? mCachedWidth - mLinesWidth[line] : 0.f; - line++; - break; - } - #ifdef EE_TEXT_SHAPER_ENABLED if ( TextShaperEnabled && mFontStyleConfig.Font->getType() == FontType::TTF ) { FontTrueType* rFont = static_cast( mFontStyleConfig.Font ); + auto layout = TextLayouter::layout( mString, rFont, mFontStyleConfig.CharacterSize, + mFontStyleConfig.Style, mTabWidth, + mFontStyleConfig.OutlineThickness ); - shapeAndRun( - mString, mFontStyleConfig, - [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t* glyphPos, Uint32 glyphCount, - const hb_segment_properties_t&, TextShapeRun& run ) { - FontTrueType* font = run.font(); - Uint32 prevGlyphIndex = 0; + mLinesWidth = std::move( layout.linesWidth ); + mCachedWidth = layout.size.getWidth(); - for ( std::size_t i = 0; i < glyphCount; ++i ) { - hb_glyph_info_t curGlyph = glyphInfo[i]; - hb_glyph_position_t curGlyphPos = glyphPos[i]; - auto curChar = mString[run.curRunStart() + curGlyph.cluster]; + for ( const ShapedGlyph& sg : layout.shapedGlyphs ) { + Float currentX = x + sg.position.x; + Float currentY = y + sg.position.y; - x += rFont->getKerningFromGlyphIndex( - prevGlyphIndex, curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, - reqItalic, mFontStyleConfig.OutlineThickness ); + line = std::floor( sg.position.y / vspace ); - if ( curChar == '\t' ) { - minX = std::min( minX, x ); + switch ( Font::getHorizontalAlign( mAlign ) ) { + case TEXT_ALIGN_CENTER: + centerDiffX = line < mLinesWidth.size() + ? std::trunc( ( mCachedWidth - mLinesWidth[line] ) * 0.5f ) + : 0.f; + break; + case TEXT_ALIGN_RIGHT: + centerDiffX = + line < mLinesWidth.size() ? mCachedWidth - mLinesWidth[line] : 0.f; + break; + } - x += - tabAdvance( hspace, mTabWidth, mTabStops ? x : std::optional{} ); + // Apply the outline + if ( mFontStyleConfig.OutlineThickness != 0 ) { + Glyph glyph = + sg.font->getGlyphByIndex( sg.glyphIndex, mFontStyleConfig.CharacterSize, bold, + reqItalic, mFontStyleConfig.OutlineThickness, + rFont->getPage( mFontStyleConfig.CharacterSize ) ); - maxX = std::max( maxX, x ); + Float left = glyph.bounds.Left; + Float top = glyph.bounds.Top; + Float right = glyph.bounds.Left + glyph.bounds.Right; + Float bottom = glyph.bounds.Top + glyph.bounds.Bottom; - if ( mCachedWidthNeedUpdate ) - maxW = std::max( maxW, x ); - - prevGlyphIndex = curGlyph.codepoint; - continue; - } - - Float currentX = x + ( curGlyphPos.x_offset / 64.f ); - Float currentY = y + ( curGlyphPos.y_offset / 64.f ); - - // Apply the outline - if ( mFontStyleConfig.OutlineThickness != 0 ) { - Glyph glyph = font->getGlyphByIndex( - curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, reqItalic, - mFontStyleConfig.OutlineThickness, - rFont->getPage( mFontStyleConfig.CharacterSize ) ); - - Float left = glyph.bounds.Left; - Float top = glyph.bounds.Top; - Float right = glyph.bounds.Left + glyph.bounds.Right; - Float bottom = glyph.bounds.Top + glyph.bounds.Bottom; - - // Add the outline glyph to the vertices - if ( glyph.bounds.Right > 0 && glyph.bounds.Bottom > 0 ) { - addGlyphQuad( mOutlineVertices, Vector2f( currentX, currentY ), glyph, - italic, mFontStyleConfig.OutlineThickness, centerDiffX ); - } - - // Update the current bounds with the outlined glyph bounds - minX = std::min( minX, x + left - italic * bottom - - mFontStyleConfig.OutlineThickness ); - maxX = std::max( maxX, x + right - italic * top - - mFontStyleConfig.OutlineThickness ); - minY = std::min( minY, y + top - mFontStyleConfig.OutlineThickness ); - maxY = std::max( maxY, y + bottom - mFontStyleConfig.OutlineThickness ); - if ( mCachedWidthNeedUpdate ) { - maxW = std::max( maxW, x + glyph.advance - italic * top - - mFontStyleConfig.OutlineThickness ); - } - } - - // Extract the current glyph's description - Glyph glyph = font->getGlyphByIndex( - curGlyph.codepoint, mFontStyleConfig.CharacterSize, bold, reqItalic, 0, - rFont->getPage( mFontStyleConfig.CharacterSize ) ); - - Float left = glyph.bounds.Left; - Float top = glyph.bounds.Top; - Float right = glyph.bounds.Left + glyph.bounds.Right; - Float bottom = glyph.bounds.Top + glyph.bounds.Bottom; - - // Add a quad for the current character - if ( glyph.bounds.Right > 0 && glyph.bounds.Bottom > 0 ) { - addGlyphQuad( mVertices, Vector2f( currentX, currentY ), glyph, italic, 0, - centerDiffX ); - } - - // Update the current bounds - minX = std::min( minX, currentX + left - italic * bottom ); - maxX = std::max( maxX, currentX + right - italic * top ); - minY = std::min( minY, currentY + top ); - maxY = std::max( maxY, currentY + bottom ); - - // Advance to the next character - x += font->isColorEmojiFont() && ' ' != curChar ? glyph.size.getWidth() - : glyph.advance; - - prevGlyphIndex = curGlyph.codepoint; + // Add the outline glyph to the vertices + if ( glyph.bounds.Right > 0 && glyph.bounds.Bottom > 0 ) { + addGlyphQuad( mOutlineVertices, Vector2f( currentX, currentY ), glyph, italic, + mFontStyleConfig.OutlineThickness, centerDiffX ); } - // If we're using the underlined style, add the last line - if ( underlined && run.runIsNewLine() ) { - addLine( mVertices, x, y, underlineOffset, underlineThickness, 0, centerDiffX ); + // Update the current bounds with the outlined glyph bounds + minX = std::min( minX, currentX + left - italic * bottom - + mFontStyleConfig.OutlineThickness ); + maxX = std::max( maxX, currentX + right - italic * top - + mFontStyleConfig.OutlineThickness ); + minY = std::min( minY, currentY + top - mFontStyleConfig.OutlineThickness ); + maxY = std::max( maxY, currentY + bottom - mFontStyleConfig.OutlineThickness ); + maxW = std::max( maxW, currentX + glyph.advance - italic * top - + mFontStyleConfig.OutlineThickness ); + } - if ( mFontStyleConfig.OutlineThickness != 0 ) - addLine( mOutlineVertices, x, y, underlineOffset, underlineThickness, - mFontStyleConfig.OutlineThickness, centerDiffX ); + // Extract the current glyph's description + Glyph glyph = sg.font->getGlyphByIndex( + sg.glyphIndex, mFontStyleConfig.CharacterSize, bold, reqItalic, 0, + rFont->getPage( mFontStyleConfig.CharacterSize ) ); + + Float left = glyph.bounds.Left; + Float top = glyph.bounds.Top; + Float right = glyph.bounds.Left + glyph.bounds.Right; + Float bottom = glyph.bounds.Top + glyph.bounds.Bottom; + + // Add a quad for the current character + if ( glyph.bounds.Right > 0 && glyph.bounds.Bottom > 0 ) { + addGlyphQuad( mVertices, Vector2f( currentX, currentY ), glyph, italic, 0, + centerDiffX ); + } + + // Update the current bounds + minX = std::min( minX, currentX + left - italic * bottom ); + maxX = std::max( maxX, currentX + right - italic * top ); + minY = std::min( minY, currentY + top ); + maxY = std::max( maxY, currentY + bottom ); + maxW = std::max( maxW, currentX + glyph.advance - italic * top ); + } + + // If we're using the underlined style, add the last line + if ( underlined ) { + Float lineTop = y; + + for ( size_t lineIdx = 0; lineIdx < mLinesWidth.size(); lineIdx++ ) { + switch ( Font::getHorizontalAlign( mAlign ) ) { + case TEXT_ALIGN_CENTER: + centerDiffX = + lineIdx < mLinesWidth.size() + ? std::trunc( ( mCachedWidth - mLinesWidth[lineIdx] ) * 0.5f ) + : 0.f; + break; + case TEXT_ALIGN_RIGHT: + centerDiffX = lineIdx < mLinesWidth.size() + ? mCachedWidth - mLinesWidth[lineIdx] + : 0.f; + break; } - // If we're using the strike through style, add the last line across all characters - if ( strikeThrough && run.runIsNewLine() ) { - addLine( mVertices, x, y, strikeThroughOffset, underlineThickness, 0, - centerDiffX ); + addLine( mVertices, mLinesWidth[lineIdx], lineTop, underlineOffset, + underlineThickness, 0, centerDiffX ); - if ( mFontStyleConfig.OutlineThickness != 0 ) - addLine( mOutlineVertices, x, y, strikeThroughOffset, underlineThickness, - mFontStyleConfig.OutlineThickness, centerDiffX ); + if ( mFontStyleConfig.OutlineThickness != 0 ) + addLine( mOutlineVertices, mLinesWidth[lineIdx], lineTop, underlineOffset, + underlineThickness, mFontStyleConfig.OutlineThickness, centerDiffX ); + + lineTop += vspace; + } + } + + // If we're using the strike through style, add the last line across all characters + if ( strikeThrough ) { + Float lineTop = y; + + for ( size_t lineIdx = 0; lineIdx < mLinesWidth.size(); lineIdx++ ) { + switch ( Font::getHorizontalAlign( mAlign ) ) { + case TEXT_ALIGN_CENTER: + centerDiffX = + lineIdx < mLinesWidth.size() + ? std::trunc( ( mCachedWidth - mLinesWidth[lineIdx] ) * 0.5f ) + : 0.f; + break; + case TEXT_ALIGN_RIGHT: + centerDiffX = lineIdx < mLinesWidth.size() + ? mCachedWidth - mLinesWidth[lineIdx] + : 0.f; + break; } - if ( mCachedWidthNeedUpdate ) - mLinesWidth.push_back( x ); + addLine( mVertices, mLinesWidth[lineIdx], lineTop, strikeThroughOffset, + underlineThickness, 0, centerDiffX ); - // next line - if ( run.runIsNewLine() ) { - y += vspace; - x = 0; - switch ( Font::getHorizontalAlign( mAlign ) ) { - case TEXT_ALIGN_CENTER: - centerDiffX = - line < mLinesWidth.size() - ? (Float)( (Int32)( ( mCachedWidth - mLinesWidth[line] ) * - 0.5f ) ) - : 0.f; - line++; - break; - case TEXT_ALIGN_RIGHT: - centerDiffX = - line < mLinesWidth.size() ? mCachedWidth - mLinesWidth[line] : 0.f; - line++; - break; - } - } + if ( mFontStyleConfig.OutlineThickness != 0 ) + addLine( mOutlineVertices, mLinesWidth[lineIdx], lineTop, strikeThroughOffset, + underlineThickness, mFontStyleConfig.OutlineThickness, centerDiffX ); - return true; - } ); + lineTop += vspace; + } + } // Update the bounding rectangle mBounds.Left = minX; @@ -2097,14 +2149,30 @@ void Text::ensureGeometryUpdate() { } #endif + switch ( Font::getHorizontalAlign( mAlign ) ) { + case TEXT_ALIGN_CENTER: + centerDiffX = line < mLinesWidth.size() + ? (Float)( (Int32)( ( mCachedWidth - mLinesWidth[line] ) * 0.5f ) ) + : 0.f; + line++; + break; + case TEXT_ALIGN_RIGHT: + centerDiffX = line < mLinesWidth.size() ? mCachedWidth - mLinesWidth[line] : 0.f; + line++; + break; + } + for ( std::size_t i = 0; i < size; ++i ) { Uint32 curChar = mString[i]; // Apply the kerning offset - x += - mFontStyleConfig.Font->getKerning( prevChar, curChar, mFontStyleConfig.CharacterSize, - bold, reqItalic, mFontStyleConfig.OutlineThickness ); - prevChar = curChar; + if ( curChar != '\n' && curChar != '\r' ) { + x += mFontStyleConfig.Font->getKerning( prevChar, curChar, + mFontStyleConfig.CharacterSize, bold, reqItalic, + mFontStyleConfig.OutlineThickness ); + prevChar = curChar; + } else + prevChar = 0; // If we're using the underlined style and there's a new line, draw a line if ( underlined && ( curChar == L'\n' ) ) { diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 21a32934e..69b9da3f6 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -2080,7 +2080,8 @@ Float UICodeEditor::getLineWidth( const Int64& docLine ) { auto len = i + 1 < vline.visualLines.size() ? vline.visualLines[i + 1].column() : line.size(); auto vlineStr = line.view().substr( pos, len - pos ); - auto curWidth = getTextWidth( vlineStr, isMonospaceLine ); + auto curWidth = + getTextWidth( vlineStr, isMonospaceLine, {}, mDoc->line( docLine ).getTextHints() ); width = eemax( width, curWidth ); } @@ -2096,13 +2097,15 @@ Float UICodeEditor::getLineWidth( const Int64& docLine ) { auto found = mLinesWidthCache.find( docLine ); if ( found != mLinesWidthCache.end() && line.getHash() == found->second.first ) return found->second.second; - Float width = getTextWidth( line.getText(), {}, mTabStops ? 0 : std::optional{} ); + Float width = getTextWidth( line.getText(), {}, mTabStops ? 0 : std::optional{}, + line.getTextHints() ); mLinesWidthCache[docLine] = { line.getHash(), width }; return width; } return getTextWidth( mDoc->line( docLine ).getText(), isMonospaceLine, - mTabStops ? 0 : std::optional{} ); + mTabStops ? 0 : std::optional{}, + mDoc->line( docLine ).getTextHints() ); } void UICodeEditor::updateScrollBar() { @@ -2530,12 +2533,12 @@ Vector2d UICodeEditor::getTextPositionOffset( const TextPosition& position, const auto& line = mDoc->line( position.line() ).getText(); auto partialLine = line.view().substr( info.range.start().column(), info.range.end().column() ); - Float x = - Text::findCharacterPos( position.column() - info.range.start().column(), mFont, - getCharacterSize(), partialLine, mFontStyleConfig.Style, - mTabWidth, mFontStyleConfig.OutlineThickness, - mTabStops ? 0 : std::optional(), false ) - .x; + Float x = Text::findCharacterPos( + position.column() - info.range.start().column(), mFont, + getCharacterSize(), partialLine, mFontStyleConfig.Style, mTabWidth, + mFontStyleConfig.OutlineThickness, mTabStops ? 0 : std::optional(), + false, mDoc->line( position.line() ).getTextHints() ) + .x; if ( visualizeNewLine && allowVisualLineEnd && position.column() == (Int64)mDoc->line( position.line() ).getText().size() - 1 ) x += getGlyphWidth(); @@ -2567,7 +2570,8 @@ Vector2d UICodeEditor::getTextPositionOffset( const TextPosition& position, Text::findCharacterPos( isLastChar ? position.column() - 1 : position.column(), mFont, getCharacterSize(), mDoc->line( position.line() ).getText(), mFontStyleConfig.Style, mTabWidth, - mFontStyleConfig.OutlineThickness, mTabStops ? 0 : std::optional(), false ) + mFontStyleConfig.OutlineThickness, mTabStops ? 0 : std::optional(), false, + mDoc->line( position.line() ).getTextHints() ) .x; if ( visualizeNewLine && isLastChar ) x += getGlyphWidth(); @@ -2600,34 +2604,36 @@ size_t UICodeEditor::characterWidth( const String& str ) const { return characterWidth( str ); } -Float UICodeEditor::getTextWidth( const String& text, std::optional tabOffset ) const { - return getTextWidth( text, false, tabOffset ); +Float UICodeEditor::getTextWidth( const String& text, std::optional tabOffset, + Uint32 textHints ) const { + return getTextWidth( text, false, tabOffset, textHints ); } size_t UICodeEditor::characterWidth( const String::View& str ) const { return characterWidth( str ); } -Float UICodeEditor::getTextWidth( const String::View& text, std::optional tabOffset ) const { - return getTextWidth( text, false, tabOffset ); +Float UICodeEditor::getTextWidth( const String::View& text, std::optional tabOffset, + Uint32 textHints ) const { + return getTextWidth( text, false, tabOffset, textHints ); } Float UICodeEditor::getTextWidth( const String& text, bool fromMonospaceLine, - std::optional tabOffset ) const { - return getTextWidth( text, fromMonospaceLine, tabOffset ); + std::optional tabOffset, Uint32 textHints ) const { + return getTextWidth( text, fromMonospaceLine, tabOffset, textHints ); } Float UICodeEditor::getTextWidth( const String::View& text, bool fromMonospaceLine, - std::optional tabOffset ) const { - return getTextWidth( text, fromMonospaceLine, tabOffset ); + std::optional tabOffset, Uint32 textHints ) const { + return getTextWidth( text, fromMonospaceLine, tabOffset, textHints ); } template Float UICodeEditor::getTextWidth( const StringType& line, bool fromMonospaceLine, - std::optional tabOffset ) const { + std::optional tabOffset, Uint32 textHints ) const { if ( !fromMonospaceLine && isNotMonospace() ) { return Text::getTextWidth( mFont, getCharacterSize(), line, mFontStyleConfig.Style, - mTabWidth, 0.f, 0, tabOffset ); + mTabWidth, 0.f, textHints, tabOffset ); } Float glyphWidth = getGlyphWidth(); @@ -3204,7 +3210,8 @@ Int64 UICodeEditor::getColFromXOffset( VisibleIndex visibleIndex, const Float& x Text::findCharacterFromPos( Vector2i( eemax( -xOffset + x, 0.f ), 0 ), true, mFont, getCharacterSize(), line, mFontStyleConfig.Style, mTabWidth, 0.f, - mTabStops ? 0 : std::optional() ); + mTabStops ? 0 : std::optional(), + mDoc->line( pos.line() ).getTextHints() ); } Int64 len = line.length(); @@ -3230,7 +3237,8 @@ Int64 UICodeEditor::getColFromXOffset( VisibleIndex visibleIndex, const Float& x if ( !isMonospaceLine( pos.line() ) ) { return Text::findCharacterFromPos( Vector2i( x, 0 ), true, mFont, getCharacterSize(), mDoc->line( pos.line() ).getText(), - mFontStyleConfig.Style, mTabWidth, 0.f, mTabStops ? 0 : std::optional() ); + mFontStyleConfig.Style, mTabWidth, 0.f, mTabStops ? 0 : std::optional(), + mDoc->line( pos.line() ).getTextHints() ); } const String& line = mDoc->line( pos.line() ).getText(); @@ -4193,6 +4201,7 @@ std::vector UICodeEditor::getTextRangeRectangles( } } selRect.Right = startScroll.x + endOffset.x; + selRect.normalize(); rects.push_back( selRect ); } } else { @@ -4232,6 +4241,7 @@ std::vector UICodeEditor::getTextRangeRectangles( lh, false, visualizeNewLines ) .x; } + selRect.normalize(); rects.push_back( selRect ); } } @@ -5365,7 +5375,8 @@ void UICodeEditor::setTabIndentAlignment( CharacterAlignment alignment ) { } bool UICodeEditor::isMonospaceLine( Int64 lineIndex ) const { - return mFont && ( mFont->isMonospace() || + return mFont && ( ( mFont->isMonospace() && + ( !Text::TextShaperEnabled || mDoc->line( lineIndex ).isAscii() ) ) || ( mFont->getType() == FontType::TTF && static_cast( mFont )->isIdentifiedAsMonospace() && mDoc->line( lineIndex ).isAscii() ) ); diff --git a/src/tests/ui_perf_test/ui_perf_test.cpp b/src/tests/ui_perf_test/ui_perf_test.cpp index 2e18e6027..2ec944681 100644 --- a/src/tests/ui_perf_test/ui_perf_test.cpp +++ b/src/tests/ui_perf_test/ui_perf_test.cpp @@ -152,6 +152,42 @@ void mainLoop() { } EE_MAIN_FUNC int main( int, char*[] ) { + { + Text::TextShaperEnabled = 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(); + 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" ) ); */ + 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-flags.uext" ); + // editor->loadFromFile( "unit_tests/assets/textformat/english.utf8.lf.nobom.txt" ); + // editor->getDocument().textInput( "اسمي..." ); + // editor->getDocument().textInput( " হ্যাঁ " ); + editor->setFont( app.getUI()->getUIThemeManager()->getDefaultFont() ); + editor->on( Event::KeyUp, [&]( const Event* event ) { + if ( event->asKeyEvent()->getKeyCode() == KEY_F1 ){ + Text::TextShaperEnabled = !Text::TextShaperEnabled; + app.getUI()->getRoot()->invalidateDraw(); + } + } ); + return app.run(); + } + win = Engine::instance()->createWindow( WindowSettings( 1366, 768, "eepp - UI Perf Test" ), ContextSettings( false ) ); @@ -199,50 +235,50 @@ EE_MAIN_FUNC int main( int, char*[] ) { ->setDefaultFont( font ) ->add( theme ); -/* - auto* vlay = UILinearLayout::NewVertical(); - vlay->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + /* + auto* vlay = UILinearLayout::NewVertical(); + vlay->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); - Clock clock; - auto model = FileSystemModel::New( "." ); // std::make_shared(); - // UITreeView* view = UITreeView::New(); - UITableView* view = UITableView::New(); - // view->setExpanderIconSize( PixelDensity::dpToPx( 20 ) ); - view->setId( "treeview" ); - view->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); - view->setParent( vlay ); - view->setModel( SortingProxyModel::New( model ) ); - Log::notice( "Total time: %.2fms", clock.getElapsedTime().asMilliseconds() ); - */ + Clock clock; + auto model = FileSystemModel::New( "." ); // std::make_shared(); + // UITreeView* view = UITreeView::New(); + UITableView* view = UITableView::New(); + // view->setExpanderIconSize( PixelDensity::dpToPx( 20 ) ); + view->setId( "treeview" ); + view->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + view->setParent( vlay ); + view->setModel( SortingProxyModel::New( model ) ); + Log::notice( "Total time: %.2fms", clock.getElapsedTime().asMilliseconds() ); + */ /* ListBox test */ -/* - std::vector strings; - for ( size_t i = 0; i < 10000; i++ ) - strings.emplace_back( String::format( - "This is a very long string number %ld. Cover the full width of the listbox.", - i ) ); - auto* lbox = UIListBox::New(); - std::cout << "Time New: " << clock.getElapsedTime().asMilliseconds() << " ms" << std::endl; - lbox->setParent( vlay ); - std::cout << "Time setParent: " << clock.getElapsedTime().asMilliseconds() << " ms" - << std::endl; - lbox->setLayoutMargin( Rectf( 4, 4, 4, 4 ) ); - std::cout << "Time setLayoutMargin: " << clock.getElapsedTime().asMilliseconds() << " ms" - << std::endl; - lbox->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); - std::cout << "Time setLayoutSizePolicy: " << clock.getElapsedTime().asMilliseconds() - << " ms" << std::endl; - for ( size_t i = 0; i < 10; i++ ) - lbox->addListBoxItem( String::format( - "This is a very long string number %ld. Cover the full width of the listbox.", - i ) ); - std::cout << "Time addListBoxItem: " << clock.getElapsedTime().asMilliseconds() << " ms" - << std::endl; - lbox->addListBoxItems( strings ); - std::cout << "Time addListBoxItems: " << clock.getElapsedTime().asMilliseconds() << " ms" - << std::endl; - */ + /* + std::vector strings; + for ( size_t i = 0; i < 10000; i++ ) + strings.emplace_back( String::format( + "This is a very long string number %ld. Cover the full width of the + listbox.", i ) ); auto* lbox = UIListBox::New(); std::cout << "Time New: " << + clock.getElapsedTime().asMilliseconds() << " ms" << std::endl; lbox->setParent( vlay ); + std::cout << "Time setParent: " << clock.getElapsedTime().asMilliseconds() << " ms" + << std::endl; + lbox->setLayoutMargin( Rectf( 4, 4, 4, 4 ) ); + std::cout << "Time setLayoutMargin: " << clock.getElapsedTime().asMilliseconds() << + " ms" + << std::endl; + lbox->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + std::cout << "Time setLayoutSizePolicy: " << clock.getElapsedTime().asMilliseconds() + << " ms" << std::endl; + for ( size_t i = 0; i < 10; i++ ) + lbox->addListBoxItem( String::format( + "This is a very long string number %ld. Cover the full width of the + listbox.", i ) ); std::cout << "Time addListBoxItem: " << + clock.getElapsedTime().asMilliseconds() << " ms" + << std::endl; + lbox->addListBoxItems( strings ); + std::cout << "Time addListBoxItems: " << clock.getElapsedTime().asMilliseconds() << + " ms" + << std::endl; + */ Clock total; /* Create Widget test */ @@ -277,7 +313,8 @@ EE_MAIN_FUNC int main( int, char*[] ) { but->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); but->setParent( parent )->clipEnable(); } - std::cout << "Time 10k UIPushButton total: " << total.getElapsedTime().toString() << std::endl; + std::cout << "Time 10k UIPushButton total: " << total.getElapsedTime().toString() + << std::endl; // uiSceneNode->getRoot()->closeAllChildren(); total.restart(); @@ -285,115 +322,117 @@ EE_MAIN_FUNC int main( int, char*[] ) { std::cout << "SceneManager::instance()->update(): " << total.getElapsedTime().toString() << std::endl; -/* - auto* main = UIRelativeLayout::New(); - main->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); - auto* sv = UIScrollView::New(); - sv->setParent( main ); - sv->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); - sv->setPixelsSize( win->getSize().asFloat() ); - auto* vlay = UILinearLayout::NewVertical(); - vlay->setParent( sv ); - vlay->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + /* + auto* main = UIRelativeLayout::New(); + main->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + auto* sv = UIScrollView::New(); + sv->setParent( main ); + sv->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + sv->setPixelsSize( win->getSize().asFloat() ); + auto* vlay = UILinearLayout::NewVertical(); + vlay->setParent( sv ); + vlay->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); - total.restart(); - for ( size_t i = 0; i < 100000; i++ ) { - auto* widget = UIWidget::New(); - widget->setParent( vlay ); - widget->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::Fixed ); - widget->setSize( Sizef( 0, 4 ) ); - Colorf col; - col.hsv.h = Math::randf( 0, 360 ); - col.hsv.s = 1; - col.hsv.v = 1; - col.hsv.a = 1; - widget->setBackgroundColor( Color::fromHsv( col ) ); - } - std::cout << "Time UIWidget total: " << total.getElapsedTime().asMilliseconds() << " ms" - << std::endl; - */ -/* - UIWindow* wind = UIWindow::New(); - wind->setSize( 500, 500 ); - wind->setWindowFlags( UI_WIN_DEFAULT_FLAGS | UI_WIN_RESIZEABLE | UI_WIN_MAXIMIZE_BUTTON ); + total.restart(); + for ( size_t i = 0; i < 100000; i++ ) { + auto* widget = UIWidget::New(); + widget->setParent( vlay ); + widget->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::Fixed ); + widget->setSize( Sizef( 0, 4 ) ); + Colorf col; + col.hsv.h = Math::randf( 0, 360 ); + col.hsv.s = 1; + col.hsv.v = 1; + col.hsv.a = 1; + widget->setBackgroundColor( Color::fromHsv( col ) ); + } + std::cout << "Time UIWidget total: " << total.getElapsedTime().asMilliseconds() << " + ms" + << std::endl; + */ + /* + UIWindow* wind = UIWindow::New(); + wind->setSize( 500, 500 ); + wind->setWindowFlags( UI_WIN_DEFAULT_FLAGS | UI_WIN_RESIZEABLE | + UI_WIN_MAXIMIZE_BUTTON ); - UILinearLayout* layWin = UILinearLayout::NewVertical(); - layWin->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); - layWin->setParent( wind ); + UILinearLayout* layWin = UILinearLayout::NewVertical(); + layWin->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + layWin->setParent( wind ); - UILinearLayout* layPar = UILinearLayout::NewHorizontal(); - layPar->setParent( layWin ); - layPar->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ); - layPar->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); - layPar->setLayoutGravity( UI_VALIGN_CENTER | UI_HALIGN_CENTER ); - layPar->setBackgroundColor( 0x999999FF ); + UILinearLayout* layPar = UILinearLayout::NewHorizontal(); + layPar->setParent( layWin ); + layPar->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ); + layPar->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + layPar->setLayoutGravity( UI_VALIGN_CENTER | UI_HALIGN_CENTER ); + layPar->setBackgroundColor( 0x999999FF ); - UILinearLayout* lay = UILinearLayout::NewVertical(); - lay->setLayoutGravity( UI_HALIGN_CENTER | UI_VALIGN_CENTER ); - lay->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); - lay->setBackgroundColor( 0x333333FF ); - lay->setLayoutWeight( 0.7f ); + UILinearLayout* lay = UILinearLayout::NewVertical(); + lay->setLayoutGravity( UI_HALIGN_CENTER | UI_VALIGN_CENTER ); + lay->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); + lay->setBackgroundColor( 0x333333FF ); + lay->setLayoutWeight( 0.7f ); - UITextView::New() - ->setText( "Text on test 1" ) - ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) - ->setParent( lay ); - UITextView::New() - ->setText( "Text on test 2" ) - ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) - ->setParent( lay ); - UICheckBox::New() - ->setText( "Checkbox" ) - ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) - ->setParent( lay ); - UITextView::New() - ->setText( "Text on test 3" ) - ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) - ->setParent( lay ); - UITextView::New() - ->setText( "Text on test 4" ) - ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) - ->setParent( lay ); - UITextInput::New() - ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) - ->setParent( lay ); + UITextView::New() + ->setText( "Text on test 1" ) + ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) + ->setParent( lay ); + UITextView::New() + ->setText( "Text on test 2" ) + ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setParent( lay ); + UICheckBox::New() + ->setText( "Checkbox" ) + ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setParent( lay ); + UITextView::New() + ->setText( "Text on test 3" ) + ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setParent( lay ); + UITextView::New() + ->setText( "Text on test 4" ) + ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setParent( lay ); + UITextInput::New() + ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setParent( lay ); - UILinearLayout* lay2 = UILinearLayout::NewVertical(); - lay2->setId( "hardlay" ); - lay2->setLayoutGravity( UI_HALIGN_CENTER | UI_VALIGN_CENTER ); - lay2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); - lay2->setBackgroundColor( Color::Black ); - lay2->setLayoutWeight( 0.3f ); + UILinearLayout* lay2 = UILinearLayout::NewVertical(); + lay2->setId( "hardlay" ); + lay2->setLayoutGravity( UI_HALIGN_CENTER | UI_VALIGN_CENTER ); + lay2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + lay2->setBackgroundColor( Color::Black ); + lay2->setLayoutWeight( 0.3f ); - UIPushButton::New() - ->setText( "PushButton" ) - ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) - ->setLayoutGravity( UI_VALIGN_CENTER ) - ->setParent( lay2 ); - UIListBox* lbox = UIListBox::New(); - lbox->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::Fixed ) - ->setSize( 0, 105 ) - ->setParent( lay2 ); - lbox->addListBoxItems( { "This", "is", "a", "ListBox" } ); - lay2->setParent( layPar ); - lay->setParent( layPar ); + UIPushButton::New() + ->setText( "PushButton" ) + ->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setLayoutGravity( UI_VALIGN_CENTER ) + ->setParent( lay2 ); + UIListBox* lbox = UIListBox::New(); + lbox->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::Fixed ) + ->setSize( 0, 105 ) + ->setParent( lay2 ); + lbox->addListBoxItems( { "This", "is", "a", "ListBox" } ); + lay2->setParent( layPar ); + lay->setParent( layPar ); - UIDropDownList* drop = UIDropDownList::New(); - drop->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) - ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) - ->setParent( layWin ); - drop->getListBox()->addListBoxItems( { "Car", "Bus", "Plane", "Submarine" } ); - drop->getListBox()->setSelected( 0 ); - wind->show(); - */ + UIDropDownList* drop = UIDropDownList::New(); + drop->setLayoutMargin( Rectf( 10, 10, 10, 10 ) ) + ->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setParent( layWin ); + drop->getListBox()->addListBoxItems( { "Car", "Bus", "Plane", "Submarine" } ); + drop->getListBox()->setSelected( 0 ); + wind->show(); + */ win->runMainLoop( &mainLoop ); } diff --git a/src/tests/unit_tests/fontrendering.cpp b/src/tests/unit_tests/fontrendering.cpp index 89a2fe49b..eef6ca5c1 100644 --- a/src/tests/unit_tests/fontrendering.cpp +++ b/src/tests/unit_tests/fontrendering.cpp @@ -1,7 +1,7 @@ -#include "eepp/ui/uithememanager.hpp" #include "utest.hpp" #include +#include #include #include #include @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -52,7 +53,10 @@ static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Wi EXPECT_TRUE( result.areSame() ); if ( !result.areSame() ) { auto saveExt( Image::saveTypeToExtension( saveType ) ); - std::string withTextShaper = Text::TextShaperEnabled ? "_text_shape" : ""; + std::string withTextShaper = + Text::TextShaperEnabled + ? ( Text::TextShaperOptimizations ? "_text_shape_no_opt" : "_text_shape" ) + : ""; std::cerr << "Test FAILED: " << result.numDifferentPixels << " pixels differ." << std::endl; std::cerr << "Maximum perceptual difference (Delta E): " << result.maxDeltaE << std::endl; if ( !FileSystem::fileExists( "output" ) ) @@ -183,6 +187,10 @@ UTEST( FontRendering, fontsTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -211,6 +219,10 @@ UTEST( FontRendering, editorTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -236,6 +248,10 @@ UTEST( FontRendering, textEditTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -248,7 +264,7 @@ UTEST( FontRendering, tabsTest ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); auto* editor = UICodeEditor::New(); editor->setPixelsSize( app.getUI()->getPixelsSize() ); - editor->loadFromFile( "assets/fontrendering/tabs_test.txt" ); + editor->loadFromFile( "assets/textfiles/test-tabs.txt" ); SceneManager::instance()->update(); SceneManager::instance()->draw(); compareImages( utest_state, utest_result, app.getWindow(), @@ -262,6 +278,10 @@ UTEST( FontRendering, tabsTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -275,7 +295,7 @@ UTEST( FontRendering, tabStopTest ) { auto* editor = UICodeEditor::New(); editor->setTabStops( true ); editor->setPixelsSize( app.getUI()->getPixelsSize() ); - editor->loadFromFile( "assets/fontrendering/tabs_test.txt" ); + editor->loadFromFile( "assets/textfiles/test-tabs.txt" ); SceneManager::instance()->update(); SceneManager::instance()->draw(); compareImages( utest_state, utest_result, app.getWindow(), @@ -289,6 +309,10 @@ UTEST( FontRendering, tabStopTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -301,7 +325,7 @@ UTEST( FontRendering, tabsTextEditTest ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); auto* editor = UITextEdit::New(); editor->setPixelsSize( app.getUI()->getPixelsSize() ); - editor->loadFromFile( "assets/fontrendering/tabs_test.txt" ); + editor->loadFromFile( "assets/textfiles/test-tabs.txt" ); SceneManager::instance()->update(); SceneManager::instance()->draw(); compareImages( utest_state, utest_result, app.getWindow(), "eepp-text-edit-tabs-test" ); @@ -314,6 +338,10 @@ UTEST( FontRendering, tabsTextEditTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -327,7 +355,7 @@ UTEST( FontRendering, tabStopTextEditTest ) { auto* editor = UITextEdit::New(); editor->setTabStops( true ); editor->setPixelsSize( app.getUI()->getPixelsSize() ); - editor->loadFromFile( "assets/fontrendering/tabs_test.txt" ); + editor->loadFromFile( "assets/textfiles/test-tabs.txt" ); SceneManager::instance()->update(); SceneManager::instance()->draw(); compareImages( utest_state, utest_result, app.getWindow(), "eepp-text-edit-tab-stop-test" ); @@ -340,6 +368,10 @@ UTEST( FontRendering, tabStopTextEditTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -367,6 +399,10 @@ UTEST( FontRendering, textViewTest ) { { BoolScopedOp op( Text::TextShaperEnabled, true ); runTest(); + + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); } } @@ -377,8 +413,8 @@ UTEST( FontRendering, textEditBengaliTest ) { WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); - FontTrueType* bengaliFont = FontTrueType::New( "NotoSerifBengali-Regular", - "assets/fonts/NotoSerifBengali-Regular.ttf" ); + FontTrueType* bengaliFont = + FontTrueType::New( "NotoSansBengali-Regular", "assets/fonts/NotoSansBengali-Regular.ttf" ); FontManager::instance()->addFallbackFont( bengaliFont ); UTEST_PRINT_STEP( "Text Shaper enabled" ); auto* editor = UITextEdit::New(); @@ -396,6 +432,8 @@ UTEST( FontRendering, textSizes ) { ASSERT_TRUE_MSG( win->isOpen(), "Failed to create Window" ); + Text::TextShaperEnabled = false; + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); @@ -417,6 +455,12 @@ UTEST( FontRendering, textSizes ) { EXPECT_EQ( 96, size.getHeight() ); EXPECT_EQ( 445, Text::getTextWidth( txt, config ) ); + Vector2i topPos{ 120, 0 }; + EXPECT_EQ( 19, Text::findCharacterFromPos( topPos, true, config.Font, config.CharacterSize, + txt, 0 ) ); + EXPECT_EQ( 19, Text::findCharacterFromPos( topPos, false, config.Font, config.CharacterSize, + txt, 0 ) ); + Vector2i startPos{ 120, 7 }; EXPECT_EQ( 19, Text::findCharacterFromPos( startPos, true, config.Font, config.CharacterSize, txt, 0 ) ); @@ -427,7 +471,7 @@ UTEST( FontRendering, textSizes ) { EXPECT_EQ( 242, Text::findCharacterFromPos( middlePos, true, config.Font, config.CharacterSize, txt, 0 ) ); EXPECT_EQ( 242, Text::findCharacterFromPos( middlePos, false, config.Font, - config.CharacterSize, txt, 0 ) ); + config.CharacterSize, txt, 0 ) ); Vector2i endPos{ 120, 103 }; EXPECT_EQ( 395, Text::findCharacterFromPos( endPos, true, config.Font, config.CharacterSize, @@ -436,6 +480,8 @@ UTEST( FontRendering, textSizes ) { txt, 0 ) ); EXPECT_EQ( 18ul, Text::findLastCharPosWithinLength( txt, 120, config ) ); + EXPECT_EQ( 446ul, Text::findLastCharPosWithinLength( txt, 1000, config ) ); + Vector2f pos = Text::findCharacterPos( 19, config.Font, config.CharacterSize, txt, 0 ); EXPECT_EQ( 120, pos.x ); EXPECT_EQ( 0, pos.y ); @@ -467,5 +513,89 @@ UTEST( FontRendering, textSizes ) { runTest(); } + UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" ); + { + BoolScopedOp op( Text::TextShaperEnabled, true ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); + } + + Engine::destroySingleton(); +} + +UTEST( FontRendering, textStyles ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 230, "eepp - Text Styles", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ) ); + + ASSERT_TRUE_MSG( win->isOpen(), "Failed to create Window" ); + + Text::TextShaperEnabled = false; + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + FontFamily::loadFromRegular( font ); + + win->setClearColor( RGB( 255, 255, 255 ) ); + + FontStyleConfig config; + config.Font = font; + config.FontColor = Color::Black; + config.CharacterSize = 20; + config.OutlineColor = Color::Black; + config.ShadowColor = Color::lightgray; + + String txt( "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n" + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n" + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n" + "consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n" + "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n" + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum." ); + + const auto runTest = [&]( std::string_view styleName, Uint32 textAlign ) { + win->clear(); + Text text; + text.setStyleConfig( config ); + text.setString( txt ); + text.setAlign( textAlign ); + text.draw( 32, 32 ); + compareImages( utest_state, utest_result, win, "eepp-text-style-" + styleName ); + }; + + const auto runTestSuite = [&]( Uint32 style, std::string_view styleName, + Uint32 textAlign = TEXT_ALIGN_LEFT ) { + config.Style = style; + + UTEST_PRINT_STEP( styleName.data() ); + UTEST_PRINT_STEP( " Text Shaper disabled" ); + runTest( styleName, textAlign ); + + UTEST_PRINT_STEP( " Text Shaper enabled" ); + BoolScopedOp op( Text::TextShaperEnabled, true ); + runTest( styleName, textAlign ); + + UTEST_PRINT_STEP( " Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest( styleName, textAlign ); + }; + + runTestSuite( Text::Regular, "regular" ); + runTestSuite( Text::Bold, "bold" ); + runTestSuite( Text::Italic, "italic" ); + runTestSuite( Text::Underlined, "underline" ); + runTestSuite( Text::StrikeThrough, "strikethrough" ); + runTestSuite( Text::Shadow, "shadow" ); + config.FontColor = Color::White; + config.OutlineThickness = 1; + runTestSuite( Text::Regular, "outline" ); + config.FontColor = Color::Black; + config.OutlineThickness = 0; + runTestSuite( Text::Regular, "regular-center", TEXT_ALIGN_CENTER ); + runTestSuite( Text::Regular, "regular-right", TEXT_ALIGN_RIGHT ); + runTestSuite( Text::Underlined, "underline-center", TEXT_ALIGN_CENTER ); + runTestSuite( Text::Underlined, "underline-right", TEXT_ALIGN_RIGHT ); + runTestSuite( Text::StrikeThrough, "strikethrough-center", TEXT_ALIGN_CENTER ); + runTestSuite( Text::StrikeThrough, "strikethrough-right", TEXT_ALIGN_RIGHT ); + Engine::destroySingleton(); }