diff --git a/.ecode/project_build.json b/.ecode/project_build.json index 4bd800991..d1352fa3a 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -204,7 +204,7 @@ }, "run": [ { - "args": "--text-shaper", + "args": "", "command": "${project_root}/bin/ecode-debug", "name": "ecode-debug", "working_dir": "${project_root}/bin" diff --git a/bin/assets/fonts/NotoColorEmoji.ttf b/bin/assets/fonts/NotoColorEmoji.ttf index 2c1f10435..943741df1 100644 Binary files a/bin/assets/fonts/NotoColorEmoji.ttf and b/bin/assets/fonts/NotoColorEmoji.ttf differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-emojis-with-text.webp b/bin/unit_tests/assets/fontrendering/eepp-emojis-with-text.webp new file mode 100644 index 000000000..3176c6217 Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-emojis-with-text.webp differ diff --git a/bin/unit_tests/assets/fontrendering/eepp-fonts.webp b/bin/unit_tests/assets/fontrendering/eepp-fonts.webp index 8bd603de8..f769431f8 100644 Binary files a/bin/unit_tests/assets/fontrendering/eepp-fonts.webp and b/bin/unit_tests/assets/fontrendering/eepp-fonts.webp differ diff --git a/include/eepp/graphics/fonttruetype.hpp b/include/eepp/graphics/fonttruetype.hpp index c5f462d70..6db1b9807 100644 --- a/include/eepp/graphics/fonttruetype.hpp +++ b/include/eepp/graphics/fonttruetype.hpp @@ -158,7 +158,7 @@ class EE_API FontTrueType : public Font { protected: friend class Text; - friend class TextLayouter; + friend class TextLayout; explicit FontTrueType( const std::string& FontName ); @@ -175,17 +175,19 @@ class EE_API FontTrueType : public Font { typedef UnorderedMap GlyphDrawableTable; struct Page { - explicit Page( const Uint32 fontInternalId, const std::string& pageName ); + explicit Page( const Uint32 fontInternalId, const std::string& pageName, + const FontTrueType* font ); ~Page(); GlyphTable glyphs; ///< Table mapping code points to their corresponding glyph GlyphDrawableTable - drawables; ///> Table mapping code points to their corresponding glyph drawables. - Texture* texture; ///< Texture containing the pixels of the glyphs - unsigned int nextRow; ///< Y position of the next new row in the texture + drawables; ///> Table mapping code points to their corresponding glyph drawables. + Texture* texture; ///< Texture containing the pixels of the glyphs std::vector rows; ///< List containing the position of all the existing rows - Uint32 fontInternalId{ 0 }; + Uint32 fontInternalId{ 0 }; // The font internal id + unsigned int nextRow; ///< Y position of the next new row in the texture + const FontTrueType* font{ nullptr }; }; void cleanup(); diff --git a/src/eepp/graphics/fonttruetype.cpp b/src/eepp/graphics/fonttruetype.cpp index de36ab4ba..784bbfeca 100644 --- a/src/eepp/graphics/fonttruetype.cpp +++ b/src/eepp/graphics/fonttruetype.cpp @@ -39,6 +39,7 @@ RETAIN_SYMBOL( FT_Palette_Select ); using namespace EE; +#ifdef EE_TRUETYPE_SVG_FONT_ENABLED struct SVG_Data { NSVGrasterizer* rasterizer; }; @@ -181,6 +182,7 @@ FT_Error svg_render( FT_GlyphSlot slot, FT_Pointer* data_pointer ) { } static SVG_RendererHooks svg_hooks = { svg_init, svg_free, svg_render, svg_preset }; +#endif // FreeType callbacks that operate on a IOStream unsigned long read( FT_Stream rec, unsigned long offset, unsigned char* buffer, @@ -430,8 +432,13 @@ bool FontTrueType::setFontFace( void* _face ) { mIsBold = face->style_flags & FT_STYLE_FLAG_BOLD; mIsItalic = face->style_flags & FT_STYLE_FLAG_ITALIC; - if ( mHasSvgGlyphs ) - FT_Property_Set( static_cast( mLibrary ), "ot-svg", "svg-hooks", &svg_hooks ); + if ( mHasSvgGlyphs ) { +#ifdef EE_TRUETYPE_SVG_FONT_ENABLED + FT_Property_Set( static_cast( mLibrary ), "ot-svg", "svg-hooks", &svg_hooks ); +#else + return false; +#endif + } if ( ( mIsColorEmojiFont || mHasSvgGlyphs || mHasColrGlyphs ) && FontManager::instance()->getColorEmojiFont() == nullptr ) @@ -1175,14 +1182,23 @@ Glyph FontTrueType::loadGlyphByIndex( Uint32 index, unsigned int characterSize, const int padding = 2; Float scale = 1.f; - - if ( mIsColorEmojiFont || mIsEmojiFont ) { - scale = eemin( 1.f, (Float)characterSize / height ); - } - int destWidth = width; int destHeight = height; + if ( mIsColorEmojiFont ) { + // Browsers scale emojis slightly larger than the EM size to match the visual weight + // of the text's Ascender. + // Noto Sans Ascent is ~1.07em. Applying 1.1x scaling results in ~1.18em, + // which matches Chrome/Firefox rendering behavior for Noto Color Emoji. + Float targetSize = ( page.font != this ) + ? (Float)page.font->getAscent( characterSize ) * 1.1f + : (Float)characterSize; + + scale = eemin( 1.f, targetSize / height ); + } else if ( mIsEmojiFont ) { + scale = eemin( 1.f, (Float)characterSize / height ); + } + glyph.advance = eeceil( glyph.advance * scale ); if ( scale >= 1.f ) { @@ -1502,7 +1518,7 @@ FontTrueType::Page& FontTrueType::getPage( unsigned int characterSize ) const { name += ":bold"; if ( mIsItalic ) name += ":italic"; - mPages[characterSize] = std::make_unique( mFontInternalId, name ); + mPages[characterSize] = std::make_unique( mFontInternalId, name, this ); pageIt = mPages.find( characterSize ); } return *pageIt->second; @@ -1681,8 +1697,9 @@ bool FontTrueType::hasColrGlyphs() const { return mHasColrGlyphs; } -FontTrueType::Page::Page( const Uint32 fontInternalId, const std::string& pageName ) : - texture( NULL ), nextRow( 3 ), fontInternalId( fontInternalId ) { +FontTrueType::Page::Page( const Uint32 fontInternalId, const std::string& pageName, + const FontTrueType* font ) : + texture( NULL ), fontInternalId( fontInternalId ), nextRow( 3 ), font( font ) { // Make sure that the texture is initialized by default Image image; image.create( 128, 128, 4 ); diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index aa959cc43..0ff9fad63 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -14,7 +14,11 @@ namespace EE { namespace Graphics { +#ifdef EE_TEXT_SHAPER_ENABLED +bool Text::TextShaperEnabled = true; +#else bool Text::TextShaperEnabled = false; +#endif bool Text::TextShaperOptimizations = true; Uint32 Text::GlobalInvalidationId = 0; diff --git a/src/eepp/graphics/textlayout.cpp b/src/eepp/graphics/textlayout.cpp index 9c2122020..9ed4e8e04 100644 --- a/src/eepp/graphics/textlayout.cpp +++ b/src/eepp/graphics/textlayout.cpp @@ -283,7 +283,8 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font, } Glyph currentGlyph = currentRunFont->getGlyphByIndex( - glyphInfo[i].codepoint, characterSize, bold, italic, outlineThickness ); + glyphInfo[i].codepoint, characterSize, bold, italic, outlineThickness, + rFont->getPage( characterSize ) ); if ( ch != '\n' && ch != '\r' && !( textDrawHints & TextHints::NoKerning ) ) { @@ -340,7 +341,8 @@ TextLayout::Cache TextLayout::layout( const StringType& string, Font* font, pen.x += Font::isEmojiCodePoint( ch ) ? currentRunFont ->getGlyphByIndex( glyphInfo[i].codepoint, characterSize, - bold, italic, outlineThickness ) + bold, italic, outlineThickness, + rFont->getPage( characterSize ) ) .advance : sg.advance.x; pen.y += sg.advance.y; diff --git a/src/tests/unit_tests/fontrendering.cpp b/src/tests/unit_tests/fontrendering.cpp index 7691438b7..3036b7afb 100644 --- a/src/tests/unit_tests/fontrendering.cpp +++ b/src/tests/unit_tests/fontrendering.cpp @@ -181,7 +181,10 @@ UTEST( FontRendering, fontsTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -213,7 +216,10 @@ UTEST( FontRendering, editorTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -242,7 +248,10 @@ UTEST( FontRendering, textEditTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -272,7 +281,10 @@ UTEST( FontRendering, tabsTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -303,7 +315,10 @@ UTEST( FontRendering, tabStopTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -332,7 +347,10 @@ UTEST( FontRendering, tabsTextEditTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -362,7 +380,10 @@ UTEST( FontRendering, tabStopTextEditTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -393,7 +414,10 @@ UTEST( FontRendering, textViewTest ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -546,7 +570,10 @@ UTEST( FontRendering, textSizes ) { }; UTEST_PRINT_STEP( "Text Shaper disabled" ); - runTest(); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } UTEST_PRINT_STEP( "Text Shaper enabled" ); { @@ -608,16 +635,22 @@ UTEST( FontRendering, textStyles ) { 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 disabled" ); + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest( styleName, textAlign ); + } - UTEST_PRINT_STEP( " Text Shaper enabled w/o optimizations" ); - BoolScopedOp op2( Text::TextShaperOptimizations, false ); - 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" ); @@ -640,3 +673,58 @@ UTEST( FontRendering, textStyles ) { Engine::destroySingleton(); } + +UTEST( FontRendering, emojisWithText ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 230, "eepp - Emojis With Text", 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 ); + + FontTrueType* fontEmojiColor = FontTrueType::New( "NotoColorEmoji" ); + fontEmojiColor->loadFromFile( "../assets/fonts/NotoColorEmoji.ttf" ); + + win->setClearColor( RGB( 255, 255, 255 ) ); + + FontStyleConfig config; + config.Font = font; + config.FontColor = Color::Black; + config.CharacterSize = 16; + + String txt( + R"txt(👻 Lorem ipsum dolor sit amet, 👻 consectetur adipisicing elit, sed do eiusmod🤯 tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad😎 minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat 🤖. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur 🧐. Excepteur sint occaecat +cupidatat non proident👽, sunt in culpa qui officia deserunt mollit anim id est laborum. 😀)txt" ); + + const auto runTest = [&]() { + win->clear(); + Text text; + text.setStyleConfig( config ); + text.setString( txt ); + text.draw( 32, 32 ); + compareImages( utest_state, utest_result, win, "eepp-emojis-with-text" ); + }; + + UTEST_PRINT_STEP( " Text Shaper disabled" ); + { + BoolScopedOp op( Text::TextShaperEnabled, false ); + runTest(); + } + + UTEST_PRINT_STEP( " Text Shaper enabled" ); + BoolScopedOp op( Text::TextShaperEnabled, true ); + runTest(); + + UTEST_PRINT_STEP( " Text Shaper enabled w/o optimizations" ); + BoolScopedOp op2( Text::TextShaperOptimizations, false ); + runTest(); + + Engine::destroySingleton(); +} diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 593bdadd5..5a43e23ea 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -144,8 +144,6 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, FontTrueType::fontHintingFromString( ini.getValue( "ui", "font_hinting", "full" ) ); ui.fontAntialiasing = FontTrueType::fontAntialiasingFromString( ini.getValue( "ui", "font_antialiasing", "grayscale" ) ); - Text::TextShaperEnabled |= ini.getValueB( "ui", "text_shaper", false ); - Text::TextShaperOptimizations |= ini.getValueB( "ui", "text_shaper_optimizations", true ); ui.editorFontInInputFields = ini.getValueB( "ui", "editor_font_in_input_fields", true ); doc.trimTrailingWhitespaces = ini.getValueB( "document", "trim_trailing_whitespaces", false ); diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index f173a365a..b89651e15 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -2678,8 +2678,9 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { if ( mSplitter->curEditorExistsAndFocused() && mSplitter->getCurEditor() ) { UICodeEditor* editor = mSplitter->getCurEditor(); auto d = mSplitter->createCodeEditorInTabWidget( - mConfig.editor.openDocumentsInMainSplit ? mSplitter->getPreferredTabWidget() - : mSplitter->tabWidgetFromWidget( editor ) ); + mConfig.editor.openDocumentsInMainSplit + ? mSplitter->getPreferredTabWidget() + : mSplitter->tabWidgetFromWidget( editor ) ); if ( d.first == nullptr && d.second == nullptr && !mSplitter->getTabWidgets().empty() ) d = mSplitter->createCodeEditorInTabWidget( mSplitter->getTabWidgets()[0] ); if ( d.first != nullptr || d.second != nullptr ) { @@ -4670,8 +4671,13 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { { "first-instance" } ); #ifdef EE_TEXT_SHAPER_ENABLED - args::Flag textShaper( parser, "text-shaper", "Enables text-shaping capabilities", - { "text-shaper" } ); + args::Flag textShaper( + parser, "text-shaper", + "WARNING: Do not use this option. It will be completely " + "removed soon. It does nothing since text-shaper is enabled by default.", + { "text-shaper" } ); + args::Flag noTextShaper( parser, "no-text-shaper", "Disables text-shaping capabilities", + { "no-text-shaper" } ); #endif std::vector args; @@ -4726,8 +4732,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { } #ifdef EE_TEXT_SHAPER_ENABLED - if ( textShaper.Get() ) - Text::TextShaperEnabled = true; + if ( noTextShaper.Get() ) + Text::TextShaperEnabled = false; #endif App::InitParameters params;