diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index 24ac28578..859b38d52 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -266,6 +266,16 @@ Soll es jetzt heruntergeladen werden? Build bearbeiten Angewählten individuellen Output-Parser bearbeiten Editor-Schriftart und -größe... + Editor-Schriftfunktionen + UI-Schriftfunktionen + Kontextvarianten (calt) + Kontextabhängige Varianten, einschließlich vieler Programmierligaturen. + Standardligaturen (liga) + Typografische Kombinationen wie fi, fl und ffi, abhängig von der Schriftart. + Kontextligaturen (clig) + Ligaturen, die in bestimmten Kontexten die Lesbarkeit verbessern. + Optionale Ligaturen (dlig) + Optionale dekorative oder stilistische Ligaturen der Schriftart. Editor-Schriftgröße Editor: Schritt zurück Editor: Schritt nach vorn diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index cbe232461..b4bb94187 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -250,6 +250,16 @@ Do you want to download it now? Edit Build Edit Selected Custom Output Parser Editor Font & Size... + Editor Font Features + UI Font Features + Contextual Alternates (calt) + Context-dependent alternatives, including many programming ligatures. + Standard Ligatures (liga) + Typographic combinations such as fi, fl, and ffi, depending on the font. + Contextual Ligatures (clig) + Ligatures applied in specific contexts to improve readability. + Discretionary Ligatures (dlig) + Optional decorative or stylistic ligatures provided by the font. Editor Font Size Editor Go Back Editor Go Forward diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index e241f506c..c28f7677e 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -251,6 +251,16 @@ Voulez-vous le télécharger maintenant ? Modifier la construction Modifier l'analyseur de sortie personnalisé sélectionné Police et taille de l'éditeur... + Fonctionnalités de la police de l'éditeur + Fonctionnalités de la police de l'interface + Variantes contextuelles (calt) + Variantes dépendant du contexte, dont de nombreuses ligatures de programmation. + Ligatures standard (liga) + Combinaisons typographiques telles que fi, fl et ffi, selon la police. + Ligatures contextuelles (clig) + Ligatures appliquées dans certains contextes pour améliorer la lisibilité. + Ligatures discrétionnaires (dlig) + Ligatures décoratives ou stylistiques facultatives fournies par la police. Taille de la police de l'éditeur Éditeur : retour en arrière Éditeur : avancer diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 6bef49895..8e8d1de4e 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -185,6 +185,16 @@ 编辑构建 编辑已选的自定义输出解析器 编辑器字体和大小... + 编辑器字体特性 + 界面字体特性 + 上下文替代 (calt) + 根据上下文应用的替代字形,包括许多编程连字。 + 标准连字 (liga) + 根据字体显示 fi、fl 和 ffi 等排版组合。 + 上下文连字 (clig) + 在特定上下文中应用连字以提高可读性。 + 任意连字 (dlig) + 字体提供的可选装饰性或风格化连字。 编辑器字体大小 编辑器撤销 编辑器重做 diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp index c26d5cd7f..68251e0cd 100644 --- a/include/eepp/core/string.hpp +++ b/include/eepp/core/string.hpp @@ -1363,6 +1363,12 @@ struct TextHints { AllAscii = 1 << 0, AllLatin1 = 1 << 1, NoKerning = 1 << 2, + StandardLigatures = 1 << 3, + ContextualAlternates = 1 << 4, + ContextualLigatures = 1 << 5, + DiscretionaryLigatures = 1 << 6, + OpenTypeFeatures = + StandardLigatures | ContextualAlternates | ContextualLigatures | DiscretionaryLigatures, }; }; diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp index dc94b085e..b61c58539 100644 --- a/include/eepp/graphics/richtext.hpp +++ b/include/eepp/graphics/richtext.hpp @@ -163,6 +163,10 @@ class EE_API RichText : public Drawable { Uint32 getTabWidth() const { return mTabWidth; } + void setTextHints( Uint32 textHints ); + + Uint32 getTextHints() const { return mTextHints; } + bool setExternalFloatExclusions( const std::vector& exclusions ); const std::vector& getExternalFloatExclusions() const { @@ -482,6 +486,7 @@ class EE_API RichText : public Drawable { bool mLineWrap{ true }; WhiteSpaceWrapMode mWhiteSpaceWrapMode{ WhiteSpaceWrapMode::Normal }; Uint32 mTabWidth{ 8 }; + Uint32 mTextHints{ 0 }; }; }} // namespace EE::Graphics diff --git a/include/eepp/graphics/text.hpp b/include/eepp/graphics/text.hpp index 5900d90d8..dfbb9c102 100644 --- a/include/eepp/graphics/text.hpp +++ b/include/eepp/graphics/text.hpp @@ -42,7 +42,7 @@ class EE_API Text { }; static inline bool canSkipShaping( Uint32 textDrawHints ) { - return Text::TextShaperOptimizations && + return Text::TextShaperOptimizations && !( textDrawHints & TextHints::OpenTypeFeatures ) && ( textDrawHints & ( TextHints::AllLatin1 | TextHints::AllAscii ) ) != 0; } @@ -57,6 +57,10 @@ class EE_API Text { static FontWeight stringToFontWeight( const std::string& str ); + static Uint32 fontFeaturesFromString( const std::string& value ); + + static std::string fontFeaturesToString( Uint32 features ); + static Float getTextWidth( Font* font, const Uint32& fontSize, const String& string, const Uint32& style, const Uint32& tabWidth = 4, const Float& outlineThickness = 0.f, Uint32 textDrawHints = 0, @@ -416,6 +420,7 @@ class EE_API Text { LineWrapMode mLineWrapMode{ LineWrapMode::NoWrap }; TextDirection mDirection{ TextDirection::Unspecified }; Vector2f mInitialOffset{ 0.f, 0.f }; + Uint32 mTextDrawHints{ 0 }; mutable SmallVector mVisualLines; mutable SmallVector mLinesWidth; diff --git a/include/eepp/ui/doc/documentview.hpp b/include/eepp/ui/doc/documentview.hpp index 9ed5b720a..19c24b8d2 100644 --- a/include/eepp/ui/doc/documentview.hpp +++ b/include/eepp/ui/doc/documentview.hpp @@ -26,10 +26,12 @@ class EE_API DocumentView { bool keepIndentation{ true }; Uint32 tabWidth{ 4 }; std::optional maxCharactersWidth{}; + Uint32 textHints{ 0 }; bool tabStops{ false }; bool operator==( const Config& other ) { return mode == other.mode && keepIndentation == other.keepIndentation && - tabWidth == other.tabWidth && maxCharactersWidth == other.maxCharactersWidth; + tabWidth == other.tabWidth && maxCharactersWidth == other.maxCharactersWidth && + textHints == other.textHints && tabStops == other.tabStops; } bool operator!=( const Config& other ) { return !( *this == other ); } }; @@ -49,7 +51,8 @@ class EE_API DocumentView { const FontStyleConfig& fontStyle, Float maxWidth, LineWrapMode mode, bool keepIndentation, Uint32 tabWidth = 4, Float whiteSpaceWidth = 0.f, - bool tabStops = false, Float initialXOffset = 0.f ); + bool tabStops = false, Float initialXOffset = 0.f, + Uint32 textHints = 0 ); DocumentView( std::shared_ptr doc, FontStyleConfig fontStyle, Config config ); @@ -137,6 +140,8 @@ class EE_API DocumentView { void setTabStops( bool enabled ); + void setTextHints( Uint32 textHints ); + bool usesTabStops() const { return mConfig.tabStops; } const std::vector getDocLineToVisibleIndex() const { return mDocLineToVisibleIndex; } diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index 2b1e269a5..ba561bb7b 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -841,6 +841,14 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { bool isKerningEnabled() const; + void setLigatureFeatures( Uint32 features ); + + Uint32 getLigatureFeatures() const; + + void clearLigaturesOverride(); + + virtual void onTextHintsChanged(); + void setTextDirection( TextDirection direction ); TextDirection getTextDirection() const; @@ -909,6 +917,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { bool mAllowSelectingTextFromGutter{ true }; bool mTabStops{ false }; bool mKerningEnabled{ false }; + bool mLigaturesOverride{ false }; bool mDisableScrollInvalidation{ false }; bool mDynamicTheming{ false }; bool mUpdatingScrollBar{ false }; @@ -917,6 +926,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { Time mBlinkTime; Time mFoldsRefreshTime; Uint32 mTabWidth; + Uint32 mLigatureFeatures{ 0 }; std::atomic mHighlightWordProcessing{ false }; TextRange mLinkPosition; String mLink; @@ -1231,7 +1241,11 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { void addCursorsFromCurrentToMousePosition(); inline Uint32 getWidgetTextDrawHints() const { - return mKerningEnabled ? 0 : TextHints::NoKerning; + const Uint32 ligatureFeatures = mLigaturesOverride + ? mLigatureFeatures + : getDefaultTextHints() & TextHints::OpenTypeFeatures; + return ( mKerningEnabled ? 0 : TextHints::NoKerning ) | + ( ligatureFeatures & TextHints::OpenTypeFeatures ); } bool setInternalFontSize( const Float& size ); diff --git a/include/eepp/ui/uiconsole.hpp b/include/eepp/ui/uiconsole.hpp index 68b159196..e733e3991 100644 --- a/include/eepp/ui/uiconsole.hpp +++ b/include/eepp/ui/uiconsole.hpp @@ -153,6 +153,12 @@ class EE_API UIConsole : public UIWidget, TextDocument& getDoc(); + void setLigatureFeatures( Uint32 features ); + Uint32 getLigatureFeatures() const; + void clearLigaturesOverride(); + Uint32 getTextHints() const; + virtual void onTextHintsChanged(); + Client::Type getTextDocumentClientType() { return TextDocument::Client::Core; } protected: @@ -171,6 +177,7 @@ class EE_API UIConsole : public UIWidget, std::vector mTextCache; UIFontStyleConfig mFontStyleConfig; Uint32 mMaxLogLines{ 8192 }; + Uint32 mLigatureFeatures{ 0 }; TextDocument mDoc; KeyBindings mKeyBindings; TextRange mSelection; @@ -192,6 +199,7 @@ class EE_API UIConsole : public UIWidget, Clock mBlinkTimer; Time mBlinkTime{ Seconds( 0.f ) }; bool mCursorVisible{ true }; + bool mLigaturesOverride{ false }; int mLastLogPos{ 0 }; #if EE_PLATFORM == EE_PLATFORM_ANDROID || EE_PLATFORM == EE_PLATFORM_IOS Float mQuakeModeHeightPercent{ 0.5f }; diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index b93e24206..cd7ec3e21 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -185,6 +185,14 @@ class EE_API UIRichText : public UIHTMLWidget { virtual RichText* getRichTextPtr() { return &mRichText; } + void setTextHintsOverride( Uint32 value, Uint32 mask = TextHints::OpenTypeFeatures ); + + void clearTextHintsOverride(); + + Uint32 getTextHints() const; + + virtual void onTextHintsChanged(); + protected: RichText mRichText; Int64 mSelCurInit{ 0 }; @@ -197,6 +205,8 @@ class EE_API UIRichText : public UIHTMLWidget { mutable Float mTextIndentPxCache{ 0 }; mutable bool mTextIndentPxDirty{ true }; Uint32 mTabSize{ 8 }; + Uint32 mTextHintsOverride{ 0 }; + Uint32 mTextHintsOverrideMask{ 0 }; WhiteSpaceCollapse mWhiteSpaceCollapse{ WhiteSpaceCollapse::Collapse }; bool mLineWrap{ true }; TextTransform::Value mTextTransform{ TextTransform::None }; diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index b0091d5a3..7964f009d 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -742,6 +742,14 @@ class EE_API UISceneNode : public SceneNode { */ void setContrastPreference( const ContrastPreference& contrastPreference ); + /** Sets the OpenType feature hints inherited by text-producing widgets in this scene. */ + void setDefaultTextHints( Uint32 textHints ); + + Uint32 getDefaultTextHints() const; + + static Uint32 resolveTextHints( Uint32 defaultHints, Uint32 overrideValue, + Uint32 overrideMask ); + /** * @brief Gets the maximum invalidation depth. * @@ -983,6 +991,7 @@ class EE_API UISceneNode : public SceneNode { std::vector mChildUISceneNodes; ColorSchemePreference mColorSchemePreference{ ColorSchemePreference::Dark }; ContrastPreference mContrastPreference{ ContrastPreference::NoPreference }; + Uint32 mDefaultTextHints{ 0 }; Uint32 mMaxInvalidationDepth{ 3 }; Node* mCurParent{ nullptr }; UISceneNode* mHostUISceneNode{ nullptr }; diff --git a/include/eepp/ui/uitextinput.hpp b/include/eepp/ui/uitextinput.hpp index c2d4ada97..09cbd1a98 100644 --- a/include/eepp/ui/uitextinput.hpp +++ b/include/eepp/ui/uitextinput.hpp @@ -134,6 +134,8 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { Client::Type getTextDocumentClientType() { return TextDocument::Client::Core; } protected: + virtual void onTextHintsChanged(); + TextDocument mDoc; Float mWaitCursorTime; Vector2f mCurPos; diff --git a/include/eepp/ui/uitextnode.hpp b/include/eepp/ui/uitextnode.hpp index 67fe66ed6..e317ec47a 100644 --- a/include/eepp/ui/uitextnode.hpp +++ b/include/eepp/ui/uitextnode.hpp @@ -37,10 +37,17 @@ class EE_API UITextNode : public UIWidget { Float getBaseline() const; + void setTextHintsOverride( Uint32 value, Uint32 mask = TextHints::OpenTypeFeatures ); + void clearTextHintsOverride(); + Uint32 getTextHints() const; + virtual void onTextHintsChanged(); + protected: String mText; size_t mLayoutCharCount{ 0 }; Text* mFlexText{ nullptr }; + Uint32 mTextHintsOverride{ 0 }; + Uint32 mTextHintsOverrideMask{ 0 }; UITextNode(); }; diff --git a/include/eepp/ui/uitextview.hpp b/include/eepp/ui/uitextview.hpp index eb695a4d0..3012a95ea 100644 --- a/include/eepp/ui/uitextview.hpp +++ b/include/eepp/ui/uitextview.hpp @@ -134,6 +134,14 @@ class EE_API UITextView : public UIWidget { void setTextSelectionRange( TextSelectionRange range ); + void setTextHintsOverride( Uint32 value, Uint32 mask = TextHints::OpenTypeFeatures ); + + void clearTextHintsOverride(); + + Uint32 getTextHints() const; + + virtual void onTextHintsChanged(); + protected: Text mTextCache; String mString; @@ -142,6 +150,8 @@ class EE_API UITextView : public UIWidget { Int32 mSelCurInit; Int32 mSelCurEnd; Uint32 mTextDrawHints{ 0 }; + Uint32 mTextHintsOverride{ 0 }; + Uint32 mTextHintsOverrideMask{ 0 }; SmallVector mSelRectsCache; Int32 mLastSelCurInit; Int32 mLastSelCurEnd; diff --git a/include/eepp/ui/uitooltip.hpp b/include/eepp/ui/uitooltip.hpp index ab2fcfa3b..2f8b8ea4a 100644 --- a/include/eepp/ui/uitooltip.hpp +++ b/include/eepp/ui/uitooltip.hpp @@ -135,6 +135,14 @@ class EE_API UITooltip : public UIWidget { bool isWordWrap() const; + void setTextHintsOverride( Uint32 value, Uint32 mask = TextHints::OpenTypeFeatures ); + + void clearTextHintsOverride(); + + Uint32 getTextHints() const; + + virtual void onTextHintsChanged(); + protected: Text* mTextCache{ nullptr }; UIFontStyleConfig mStyleConfig; @@ -145,6 +153,8 @@ class EE_API UITooltip : public UIWidget { TextTransform::Value mTextTransform{ TextTransform::None }; bool mDontAutoHideOnMouseMove{ false }; bool mUsingCustomStyling{ false }; + Uint32 mTextHintsOverride{ 0 }; + Uint32 mTextHintsOverrideMask{ 0 }; UITooltip(); diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp index 49d0db5f8..84db6474c 100644 --- a/include/eepp/ui/uiwidget.hpp +++ b/include/eepp/ui/uiwidget.hpp @@ -94,6 +94,12 @@ class EE_API UIWidget : public UINode { return UIWidget::getType() == type || UINode::isType( type ); } + /** Returns the scene-level text rendering hints inherited by this widget. */ + Uint32 getDefaultTextHints() const; + + /** Called when the scene-level text rendering hints change. */ + virtual void onTextHintsChanged(); + /** * @brief Sets multiple flags on the widget. * diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index 749af1511..88c4b27a9 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -502,6 +502,17 @@ static void setInlineItemTextTabWidth( std::vector& items, } } +static void setInlineItemTextHints( std::vector& items, Uint32 textHints ) { + for ( auto& item : items ) { + if ( item.isTextRun() ) { + if ( item.asTextRun().text ) + item.asTextRun().text->setTextHints( textHints ); + } else if ( item.isBox() ) { + setInlineItemTextHints( item.asBox().children, textHints ); + } + } +} + struct InlineAncestorRef { RichText::RenderSpan::InlinePath path; const RichText::InlineItem::Box* box{ nullptr }; @@ -1947,6 +1958,7 @@ class RichTextInlineLayouter { renderStyle.Style |= inlineAncestorTextDecoration( inlineItems, payload.inlinePath ); renderSpanText->setStyleConfig( renderStyle ); renderSpanText->setTabWidth( payload.text->getTabWidth() ); + renderSpanText->setTextHints( payload.text->getTextHints() & TextHints::OpenTypeFeatures ); Float height = getTextVisualLineHeight( payload.text, payload.lineHeight ); Float lineHeight = getTextRunLineHeight( payload.text, payload.lineHeight ); @@ -2313,6 +2325,7 @@ void RichText::addInlineTextImpl( TextType&& text, const FontStyleConfig& style, setTextString( *run.text, std::forward( text ) ); run.text->setStyleConfig( style ); run.text->setTabWidth( mTabWidth ); + run.text->setTextHints( mTextHints ); run.source = source; run.margin = margin; run.padding = padding; @@ -2444,6 +2457,14 @@ void RichText::setTabWidth( Uint32 tabWidth ) { } } +void RichText::setTextHints( Uint32 textHints ) { + if ( mTextHints != textHints ) { + mTextHints = textHints; + setInlineItemTextHints( mInlineItems, textHints ); + invalidateLayout(); + } +} + bool RichText::setExternalFloatExclusions( const std::vector& exclusions ) { if ( mExternalFloatExclusions == exclusions ) return false; diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index cb244a243..16091a548 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -24,6 +24,43 @@ bool Text::TextShaperEnabled = false; bool Text::TextShaperOptimizations = true; Uint32 Text::GlobalInvalidationId = 0; +Uint32 Text::fontFeaturesFromString( const std::string& value ) { + Uint32 features = 0; + String::splitCb( + [&features]( std::string_view feature ) { + feature = String::trim( feature, " \t'\"" ); + if ( String::iequals( feature, "liga" ) ) + features |= TextHints::StandardLigatures; + else if ( String::iequals( feature, "calt" ) ) + features |= TextHints::ContextualAlternates; + else if ( String::iequals( feature, "clig" ) ) + features |= TextHints::ContextualLigatures; + else if ( String::iequals( feature, "dlig" ) ) + features |= TextHints::DiscretionaryLigatures; + return true; + }, + value, ",", "", "" ); + return features; +} + +std::string Text::fontFeaturesToString( Uint32 features ) { + std::string value; + const auto append = [&value]( const char* feature ) { + if ( !value.empty() ) + value += ','; + value += feature; + }; + if ( features & TextHints::StandardLigatures ) + append( "liga" ); + if ( features & TextHints::ContextualAlternates ) + append( "calt" ); + if ( features & TextHints::ContextualLigatures ) + append( "clig" ); + if ( features & TextHints::DiscretionaryLigatures ) + append( "dlig" ); + return value; +} + Float Text::tabAdvance( Float hspace, Uint32 tabWidth, std::optional tabOffset ) { Float advance = hspace * tabWidth; if ( tabOffset ) { @@ -661,7 +698,7 @@ void Text::onNewString() { mGeometryNeedUpdate = true; mCachedWidthNeedUpdate = true; mVisualLinesNeedUpdate = true; - mTextHints = mString.getTextHints(); + mTextHints = mString.getTextHints() | mTextDrawHints; checkColorEmojis(); } @@ -1197,6 +1234,61 @@ Vector2f Text::findCharacterPos( std::size_t index, Font* font, const Uint32& fo .trunc(); } + // HarfBuzz can map several source characters to a single glyph cluster. Find the cluster + // surrounding the requested grapheme boundary in one pass, regardless of visual order. + const ShapedGlyph* caretCluster = nullptr; + std::size_t clusterStart = 0; + std::size_t clusterEnd = string.size(); + Float clusterLeft = 0; + Float clusterRight = 0; + bool hasExactCluster = false; + for ( const ShapedTextParagraph& sp : layout->paragraphs ) { + for ( std::size_t i = 0; i < sp.shapedGlyphs.size(); ) { + const std::size_t currentStart = sp.shapedGlyphs[i].stringIndex; + Float currentLeft = sp.shapedGlyphs[i].position.x; + Float currentRight = currentLeft + sp.shapedGlyphs[i].advance.x; + std::size_t j = i + 1; + while ( j < sp.shapedGlyphs.size() && + sp.shapedGlyphs[j].stringIndex == currentStart ) { + currentLeft = std::min( currentLeft, sp.shapedGlyphs[j].position.x ); + currentRight = std::max( currentRight, sp.shapedGlyphs[j].position.x + + sp.shapedGlyphs[j].advance.x ); + ++j; + } + if ( currentStart < index && ( !caretCluster || currentStart > clusterStart ) ) { + caretCluster = &sp.shapedGlyphs[i]; + clusterStart = currentStart; + clusterLeft = currentLeft; + clusterRight = currentRight; + } + if ( currentStart == index ) + hasExactCluster = true; + if ( currentStart > index ) + clusterEnd = std::min( clusterEnd, currentStart ); + i = j; + } + } + if ( caretCluster && !hasExactCluster && index < clusterEnd && + string.isGraphemeBoundary( index ) ) { + std::size_t boundaryCount = 0; + std::size_t boundaryIndex = 0; + for ( std::size_t boundary = clusterStart + 1; boundary <= clusterEnd; ++boundary ) { + if ( string.isGraphemeBoundary( boundary ) ) { + ++boundaryCount; + if ( boundary <= index ) + boundaryIndex = boundaryCount; + } + } + if ( boundaryCount > 0 ) { + const Float ratio = + static_cast( boundaryIndex ) / static_cast( boundaryCount ); + const Float x = caretCluster->direction == TextDirection::RightToLeft + ? clusterRight - ( clusterRight - clusterLeft ) * ratio + : clusterLeft + ( clusterRight - clusterLeft ) * ratio; + return Vector2f{ x, caretCluster->position.y + initialOffset.y }.trunc(); + } + } + Uint32 maxStringIndex = 0; Uint32 closestDist = std::numeric_limits::max(); @@ -1389,6 +1481,7 @@ Int32 Text::findCharacterFromPos( const Vector2i& pos, bool returnNearest, Font* for ( auto i = 0; i < sgs; i++ ) { const ShapedGlyph* sg = &sp.shapedGlyphs[i]; + const ShapedGlyph* firstClusterGlyph = sg; charLeft = sg->position.x; charTop = sg->position.y; @@ -1399,20 +1492,21 @@ Int32 Text::findCharacterFromPos( const Vector2i& pos, bool returnNearest, Font* while ( i + 1 < sgs && sp.shapedGlyphs[i + 1].stringIndex == sg->stringIndex ) { i++; sg = &sp.shapedGlyphs[i]; + charLeft = std::min( charLeft, sg->position.x ); charBottom = sg->position.y + vspace; - charRight = sg->position.x + sg->advance.x; + charRight = std::max( charRight, sg->position.x + sg->advance.x ); }; if ( fpos.y >= charTop && fpos.y <= charBottom ) { auto findNextInsertionIndex = [&]() -> Int32 { - if ( layout->isRTL() ) { + if ( firstClusterGlyph->direction == TextDirection::RightToLeft ) { if ( i > 0 ) { for ( auto j = i - 1; j >= 0; j-- ) { if ( sp.shapedGlyphs[j].stringIndex > sg->stringIndex ) return sp.shapedGlyphs[j].stringIndex; } } - return 0; + return tSize; } else { for ( auto j = i + 1; j < sgs; ++j ) { if ( sp.shapedGlyphs[j].stringIndex > sg->stringIndex ) @@ -1423,12 +1517,31 @@ Int32 Text::findCharacterFromPos( const Vector2i& pos, bool returnNearest, Font* }; if ( fpos.x >= charLeft && fpos.x < charRight ) { - Float midPoint = charLeft + ( charRight - charLeft ) * 0.5f; - if ( fpos.x < midPoint ) { - return sg->stringIndex; - } else { - return findNextInsertionIndex(); + const Int32 nextInsertionIndex = findNextInsertionIndex(); + const std::size_t clusterStart = firstClusterGlyph->stringIndex; + const std::size_t clusterEnd = std::max( + static_cast( nextInsertionIndex ), clusterStart + 1 ); + std::size_t boundaryCount = 1; + for ( std::size_t boundary = clusterStart + 1; boundary < clusterEnd; + ++boundary ) { + if ( string.isGraphemeBoundary( boundary ) ) + ++boundaryCount; } + const Float width = charRight - charLeft; + Float ratio = width > 0 ? ( fpos.x - charLeft ) / width : 0.f; + if ( firstClusterGlyph->direction == TextDirection::RightToLeft ) + ratio = 1.f - ratio; + const std::size_t caret = static_cast( + std::round( ratio * static_cast( boundaryCount ) ) ); + if ( caret == 0 ) + return clusterStart; + std::size_t boundaryIndex = 0; + for ( std::size_t boundary = clusterStart + 1; boundary < clusterEnd; + ++boundary ) { + if ( string.isGraphemeBoundary( boundary ) && ++boundaryIndex == caret ) + return boundary; + } + return clusterEnd; } } @@ -1721,7 +1834,11 @@ Uint32 Text::getTextHints() const { } void Text::setTextHints( Uint32 textHints ) { - mTextHints = textHints; + if ( mTextDrawHints != textHints ) { + mTextDrawHints = textHints; + mTextHints = mString.getTextHints() | mTextDrawHints; + invalidate(); + } } void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Float& rotation, diff --git a/src/eepp/graphics/textlayout.cpp b/src/eepp/graphics/textlayout.cpp index 547bf5b16..8cbc9bf14 100644 --- a/src/eepp/graphics/textlayout.cpp +++ b/src/eepp/graphics/textlayout.cpp @@ -149,7 +149,7 @@ static void segmentString( TextLayout& result, String::View input, Callable cb, template static void shapeAndRun( TextLayout& result, const String& string, FontTrueType* font, Uint32 characterSize, Uint32 style, Float outlineThickness, - TextDirection baseDirection, Callable cb ) { + Uint32 textDrawHints, TextDirection baseDirection, Callable cb ) { String::View input = string.view(); hb_buffer_t* hbBuffer = getThreadLocalHbBuffer(); @@ -178,17 +178,28 @@ static void shapeAndRun( TextLayout& result, const String& string, FontTrueType* hb_buffer_guess_segment_properties( hbBuffer ); hb_segment_properties_t props; hb_buffer_get_segment_properties( hbBuffer, &props ); - std::uint32_t featuresEnabled = !isSimpleScript( segment.script ) ? 1 : 0; + const bool complexShapingFeaturesEnabled = !isSimpleScript( segment.script ); + const auto featureEnabled = [complexShapingFeaturesEnabled, + textDrawHints]( Uint32 textHint ) -> std::uint32_t { + return static_cast( complexShapingFeaturesEnabled || + ( textDrawHints & textHint ) ); + }; // We use our own kerning algo const hb_feature_t features[] = { - hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), featuresEnabled, + hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), complexShapingFeaturesEnabled, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'l', 'i', 'g', 'a' ), featuresEnabled, + hb_feature_t{ HB_TAG( 'l', 'i', 'g', 'a' ), + featureEnabled( TextHints::StandardLigatures ), HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'c', 'l', 'i', 'g' ), featuresEnabled, + hb_feature_t{ HB_TAG( 'c', 'l', 'i', 'g' ), + featureEnabled( TextHints::ContextualLigatures ), HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, - hb_feature_t{ HB_TAG( 'd', 'l', 'i', 'g' ), featuresEnabled, + hb_feature_t{ HB_TAG( 'd', 'l', 'i', 'g' ), + featureEnabled( TextHints::DiscretionaryLigatures ), + HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + hb_feature_t{ HB_TAG( 'c', 'a', 'l', 't' ), + featureEnabled( TextHints::ContextualAlternates ), HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, }; @@ -226,13 +237,16 @@ static void shapeAndRun( TextLayout& result, const String& string, FontTrueType* static inline Uint64 textLayoutHash( const String::View& string, Font* font, const Uint32& characterSize, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, - std::optional tabOffset, TextDirection direction, - LineWrapMode wrapMode, Uint32 wrapWidth, bool keepIndentation, + std::optional tabOffset, Uint32 textDrawHints, + TextDirection direction, LineWrapMode wrapMode, + Uint32 wrapWidth, bool keepIndentation, Float initialXOffset ) { return hashCombine( std::hash()( string ), std::hash()( font ), std::hash()( characterSize ), std::hash()( style ), std::hash()( tabWidth ), std::hash()( outlineThickness ), std::hash>()( tabOffset ), + std::hash()( textDrawHints & ( TextHints::NoKerning | + TextHints::OpenTypeFeatures ) ), std::hash>()( static_cast>( direction ) ), std::hash>()( @@ -285,8 +299,8 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, Uint64 hash = 0; if ( !Text::canSkipShaping( textDrawHints ) ) { hash = textLayoutHash( string, font, characterSize, style, tabWidth, outlineThickness, - tabOffset, baseDirection, wrapMode, wrapWidth, keepIndentation, - initialXOffset ); + tabOffset, textDrawHints, baseDirection, wrapMode, wrapWidth, + keepIndentation, initialXOffset ); auto cacheHit = getLayoutCache().get( hash ); if ( cacheHit.has_value() ) @@ -320,7 +334,8 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, !Text::canSkipShaping( textDrawHints ) ) { FontTrueType* rFont = static_cast( font ); shapeAndRun( - result, string, rFont, characterSize, style, outlineThickness, baseDirection, + result, string, rFont, characterSize, style, outlineThickness, textDrawHints, + baseDirection, [&]( hb_glyph_info_t* glyphInfo, hb_glyph_position_t* glyphPos, Uint32 glyphCount, const hb_segment_properties_t& props, const TextSegment& segment, TextShapeRun& run ) { diff --git a/src/eepp/ui/doc/documentview.cpp b/src/eepp/ui/doc/documentview.cpp index 7c0f4517a..ec88d4284 100644 --- a/src/eepp/ui/doc/documentview.cpp +++ b/src/eepp/ui/doc/documentview.cpp @@ -15,14 +15,14 @@ LineWrapInfo DocumentView::computeLineBreaks( const TextDocument& doc, size_t li const FontStyleConfig& fontStyle, Float maxWidth, LineWrapMode mode, bool keepIndentation, Uint32 tabWidth, Float whiteSpaceWidth, bool tabStops, - Float initialXOffset ) { + Float initialXOffset, Uint32 textHints ) { if ( line >= doc.linesCount() ) return {}; const auto& docLine = doc.line( line ); const auto& text = docLine.getText(); - return LineWrap::computeLineBreaks( text.view().substr( 0, text.size() - 1 ), fontStyle, - maxWidth, mode, keepIndentation, tabWidth, whiteSpaceWidth, - docLine.getTextHints(), tabStops, initialXOffset ); + return LineWrap::computeLineBreaks( + text.view().substr( 0, text.size() - 1 ), fontStyle, maxWidth, mode, keepIndentation, + tabWidth, whiteSpaceWidth, docLine.getTextHints() | textHints, tabStops, initialXOffset ); } DocumentView::DocumentView( std::shared_ptr doc, FontStyleConfig fontStyle, @@ -124,7 +124,8 @@ void DocumentView::invalidateCache() { } else { auto lb = wrap ? computeLineBreaks( *mDoc, i, mFontStyle, mMaxWidth, mConfig.mode, mConfig.keepIndentation, mConfig.tabWidth, - mWhiteSpaceWidth, mConfig.tabStops ) + mWhiteSpaceWidth, mConfig.tabStops, 0.f, + mConfig.textHints ) : LineWrapInfo{ { 0 }, 0.f }; mVisibleLinesOffset.emplace_back( lb.paddingStart ); bool first = true; @@ -349,8 +350,8 @@ void DocumentView::updateCache( Int64 fromLine, Int64 toLine, Int64 numLines ) { return; // Safety check: ensure fromLine and toLine are within bounds of the old state - if ( fromLine < 0 || fromLine >= (Int64)mVisibleLinesOffset.size() || - toLine < 0 || toLine >= (Int64)mVisibleLinesOffset.size() || fromLine > toLine ) { + if ( fromLine < 0 || fromLine >= (Int64)mVisibleLinesOffset.size() || toLine < 0 || + toLine >= (Int64)mVisibleLinesOffset.size() || fromLine > toLine ) { invalidateCache(); return; } @@ -370,10 +371,8 @@ void DocumentView::updateCache( Int64 fromLine, Int64 toLine, Int64 numLines ) { Int64 oldIdxTo = static_cast( toVisibleIndex( toLine, true ) ); if ( oldIdxFrom == static_cast( VisibleIndex::invalid ) || - oldIdxTo == static_cast( VisibleIndex::invalid ) || - oldIdxFrom > oldIdxTo || - oldIdxFrom >= (Int64)mVisibleLines.size() || - oldIdxTo >= (Int64)mVisibleLines.size() ) { + oldIdxTo == static_cast( VisibleIndex::invalid ) || oldIdxFrom > oldIdxTo || + oldIdxFrom >= (Int64)mVisibleLines.size() || oldIdxTo >= (Int64)mVisibleLines.size() ) { invalidateCache(); return; } @@ -412,9 +411,9 @@ void DocumentView::updateCache( Int64 fromLine, Int64 toLine, Int64 numLines ) { eemax( mMaxWidth - mWhiteSpaceWidth, mWhiteSpaceWidth ) ) ); mDocLineToVisibleIndex[i] = static_cast( VisibleIndex::invalid ); } else { - auto lb = computeLineBreaks( *mDoc, i, mFontStyle, mMaxWidth, mConfig.mode, - mConfig.keepIndentation, mConfig.tabWidth, - mWhiteSpaceWidth, mConfig.tabStops ); + auto lb = computeLineBreaks( + *mDoc, i, mFontStyle, mMaxWidth, mConfig.mode, mConfig.keepIndentation, + mConfig.tabWidth, mWhiteSpaceWidth, mConfig.tabStops, 0.f, mConfig.textHints ); mVisibleLinesOffset.insert( mVisibleLinesOffset.begin() + i, lb.paddingStart ); @@ -460,8 +459,9 @@ void DocumentView::recomputeDocLineToVisibleIndex( Int64 fromVisibleIndex, bool } if ( visibleLine.line() < (Int64)mDocLineToVisibleIndex.size() ) mDocLineToVisibleIndex[visibleLine.line()] = - isFolded( visibleLine.line(), true ) ? static_cast( VisibleIndex::invalid ) - : visibleIdx; + isFolded( visibleLine.line(), true ) + ? static_cast( VisibleIndex::invalid ) + : visibleIdx; previousLineIdx = visibleLine.line(); } } @@ -599,7 +599,8 @@ void DocumentView::changeVisibility( Int64 fromDocIdx, Int64 toDocIdx, bool visi auto lb = isWrapEnabled() ? computeLineBreaks( *mDoc, i, mFontStyle, mMaxWidth, mConfig.mode, mConfig.keepIndentation, mConfig.tabWidth, - mWhiteSpaceWidth, mConfig.tabStops ) + mWhiteSpaceWidth, mConfig.tabStops, 0.f, + mConfig.textHints ) : LineWrapInfo{ { 0 }, 0 }; if ( recomputeOffset && i < (Int64)mVisibleLinesOffset.size() ) mVisibleLinesOffset[i] = lb.paddingStart; @@ -618,9 +619,8 @@ void DocumentView::changeVisibility( Int64 fromDocIdx, Int64 toDocIdx, bool visi Int64 oldIdxFrom = static_cast( oldIdxFromVI ); Int64 oldIdxTo = static_cast( oldIdxToVI ); if ( VisibleIndex::invalid == oldIdxFromVI || VisibleIndex::invalid == oldIdxToVI || - oldIdxFrom < 0 || oldIdxTo < 0 || - oldIdxFrom > oldIdxTo || oldIdxFrom >= (Int64)mVisibleLines.size() || - oldIdxTo >= (Int64)mVisibleLines.size() ) + oldIdxFrom < 0 || oldIdxTo < 0 || oldIdxFrom > oldIdxTo || + oldIdxFrom >= (Int64)mVisibleLines.size() || oldIdxTo >= (Int64)mVisibleLines.size() ) return; mVisibleLines.erase( mVisibleLines.begin() + oldIdxFrom, mVisibleLines.begin() + oldIdxTo + 1 ); @@ -761,4 +761,11 @@ void DocumentView::setTabStops( bool enabled ) { } } +void DocumentView::setTextHints( Uint32 textHints ) { + if ( textHints != mConfig.textHints ) { + mConfig.textHints = textHints; + invalidateCache(); + } +} + }}} // namespace EE::UI::Doc diff --git a/src/eepp/ui/tools/uidiffview.cpp b/src/eepp/ui/tools/uidiffview.cpp index 2ea55a82c..6ff02dacf 100644 --- a/src/eepp/ui/tools/uidiffview.cpp +++ b/src/eepp/ui/tools/uidiffview.cpp @@ -174,7 +174,8 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { screenStart.y + textOffsetY ); Text::draw( mView->getFileName(), pos, font, fontSize, textColor, 0, 0.f, Color::Black, - Color::Black, { 1, 1 }, 4, mView->getFileName().getTextHints() ); + Color::Black, { 1, 1 }, 4, + mView->getFileName().getTextHints() | mView->getDefaultTextHints() ); } void drawBeforeLineText( UICodeEditor* editor, const Int64& index, Vector2f position, @@ -286,14 +287,15 @@ class UIDiffEditorPlugin : public UICodeEditorPlugin { FontStyleConfig config = editor->getFontStyleConfig(); config.FontColor = editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineNumber ); - Float textWidth = Text::getTextWidth( text, config, 4, TextHints::AllAscii ); + const Uint32 textHints = TextHints::AllAscii | editor->getDefaultTextHints(); + Float textWidth = Text::getTextWidth( text, config, 4, textHints ); Vector2f pos( screenStart.x + std::floor( ( mGutterWidth - textWidth ) * 0.5f ), screenStart.y + std::floor( ( lineHeight - config.Font->getLineSpacing( config.CharacterSize ) ) * 0.5f ) ); - Text::draw( text, pos, config, 4, TextHints::AllAscii ); + Text::draw( text, pos, config, 4, textHints ); } protected: diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index e10beab27..ea520fe44 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -140,7 +140,8 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis UIWidget( elementTag ), mFont( getUISceneNode()->getResourceScope()->findFont( "monospace" ).get() ), mDoc( std::make_shared() ), - mDocView( mDoc, mFontStyleConfig, { .tabStops = mTabStops } ), + mDocView( mDoc, mFontStyleConfig, + { .textHints = TextHints::NoKerning, .tabStops = mTabStops } ), mBlinkTime( Seconds( 0.5f ) ), mFoldsRefreshTime( Seconds( 2.f ) ), mTabWidth( 4 ), @@ -2388,7 +2389,8 @@ void UICodeEditor::onDocumentLineMove( const Int64& fromLine, const Int64& toLin } } - if ( !mFont || mFont->isMonospace() || mLinesWidthCache.empty() ) + if ( !mFont || ( mFont->isMonospace() && getLigatureFeatures() == 0 ) || + mLinesWidthCache.empty() ) return; Int64 linesCount = mDoc->linesCount(); @@ -4333,23 +4335,28 @@ void UICodeEditor::drawLineText( const Int64& line, Vector2f position, const Flo subText, { position.x + start * getGlyphWidth(), position.y + lineOffset }, fontStyle, mTabWidth, - getChunkHints ? String::getTextHints( subText ) : drawHints, + getChunkHints + ? String::getTextHints( subText ) | getWidgetTextDrawHints() + : drawHints, mTextDirection, whitespaceDisplayConfig ); if ( minimumCharsToCoverScreen == end ) break; } } else { String::View subText( text.substr( 0, eemin( curCharsWidth, maxWidth ) ) ); - size = Text::draw( - subText, { position.x, position.y + lineOffset }, fontStyle, mTabWidth, - getChunkHints ? String::getTextHints( subText ) : drawHints, - mTextDirection, whitespaceDisplayConfig ); + size = Text::draw( subText, { position.x, position.y + lineOffset }, + fontStyle, mTabWidth, + getChunkHints ? String::getTextHints( subText ) | + getWidgetTextDrawHints() + : drawHints, + mTextDirection, whitespaceDisplayConfig ); } } else { - size = Text::draw( text, { position.x, position.y + lineOffset }, fontStyle, - mTabWidth, - getChunkHints ? String::getTextHints( text ) : drawHints, - mTextDirection, whitespaceDisplayConfig ); + size = Text::draw( + text, { position.x, position.y + lineOffset }, fontStyle, mTabWidth, + getChunkHints ? String::getTextHints( text ) | getWidgetTextDrawHints() + : drawHints, + mTextDirection, whitespaceDisplayConfig ); } if ( !isMonospace ) @@ -4596,7 +4603,8 @@ void UICodeEditor::drawLineNumbers( const DocumentLineRange& lineRange, const Ve : mLineNumberFontColor, mFontStyleConfig.Style, mFontStyleConfig.OutlineThickness, mFontStyleConfig.OutlineColor, mFontStyleConfig.ShadowColor, - mFontStyleConfig.ShadowOffset, 4, TextHints::AllAscii ); + mFontStyleConfig.ShadowOffset, 4, + TextHints::AllAscii | getWidgetTextDrawHints() ); } if ( foldVisible && mDoc->getFoldRangeService().isFoldingRegionInLine( i ) ) { @@ -4664,7 +4672,7 @@ void UICodeEditor::drawLineNumbers( const DocumentLineRange& lineRange, const Ve .asFloat(); Text::draw( String( (String::StringBaseType)0x2026 /* … */ ), offset, fontStyle, - mTabWidth ); + mTabWidth, getWidgetTextDrawHints() ); primitives.setColor( mLineBreakColumnColor ); primitives.drawLine( @@ -5694,7 +5702,8 @@ void UICodeEditor::refreshTag() { } bool UICodeEditor::isNotMonospace() const { - return ( mFont && !mFont->isMonospace() ) || Text::TextShaperEnabled; + return getLigatureFeatures() != 0 || ( mFont && !mFont->isMonospace() ) || + Text::TextShaperEnabled; } void UICodeEditor::updateMouseCursor( const Vector2f& position ) { @@ -5723,11 +5732,12 @@ void UICodeEditor::setTabIndentAlignment( CharacterAlignment alignment ) { } bool UICodeEditor::isMonospaceLine( Int64 lineIndex ) const { - return mFont && ( ( mFont->isMonospace() && - ( !Text::TextShaperEnabled || mDoc->line( lineIndex ).isAscii() ) ) || - ( mFont->getType() == FontType::TTF && - static_cast( mFont )->isIdentifiedAsMonospace() && - mDoc->line( lineIndex ).isAscii() ) ); + return getLigatureFeatures() == 0 && mFont && + ( ( mFont->isMonospace() && + ( !Text::TextShaperEnabled || mDoc->line( lineIndex ).isAscii() ) ) || + ( mFont->getType() == FontType::TTF && + static_cast( mFont )->isIdentifiedAsMonospace() && + mDoc->line( lineIndex ).isAscii() ) ); } Float UICodeEditor::editorWidth() const { @@ -5781,6 +5791,9 @@ void UICodeEditor::setTabStops( bool enabled ) { void UICodeEditor::setKerningEnabled( bool enabled ) { if ( mKerningEnabled != enabled ) { mKerningEnabled = enabled; + mDocView.setTextHints( getWidgetTextDrawHints() ); + mLinesWidthCache.clear(); + invalidateLongestLineWidth(); invalidateDraw(); } } @@ -5789,6 +5802,33 @@ bool UICodeEditor::isKerningEnabled() const { return mKerningEnabled; } +void UICodeEditor::setLigatureFeatures( Uint32 features ) { + features &= TextHints::OpenTypeFeatures; + if ( !mLigaturesOverride || mLigatureFeatures != features ) { + mLigaturesOverride = true; + mLigatureFeatures = features; + onTextHintsChanged(); + } +} + +Uint32 UICodeEditor::getLigatureFeatures() const { + return getWidgetTextDrawHints() & TextHints::OpenTypeFeatures; +} + +void UICodeEditor::clearLigaturesOverride() { + if ( mLigaturesOverride ) { + mLigaturesOverride = false; + onTextHintsChanged(); + } +} + +void UICodeEditor::onTextHintsChanged() { + mDocView.setTextHints( getWidgetTextDrawHints() ); + mLinesWidthCache.clear(); + invalidateLongestLineWidth(); + invalidateDraw(); +} + void UICodeEditor::setTextDirection( TextDirection direction ) { if ( direction == mTextDirection ) return; diff --git a/src/eepp/ui/uiconsole.cpp b/src/eepp/ui/uiconsole.cpp index 935cdc557..f506551a8 100644 --- a/src/eepp/ui/uiconsole.cpp +++ b/src/eepp/ui/uiconsole.cpp @@ -66,6 +66,7 @@ UIConsole::UIConsole( Font* font, const bool& makeDefaultCommands, const bool& a createDefaultCommands(); mTextCache.resize( maxLinesOnScreen() ); + onTextHintsChanged(); cmdGetLog(); @@ -147,6 +148,38 @@ TextDocument& UIConsole::getDoc() { return mDoc; } +void UIConsole::setLigatureFeatures( Uint32 features ) { + features &= TextHints::OpenTypeFeatures; + if ( !mLigaturesOverride || mLigatureFeatures != features ) { + mLigaturesOverride = true; + mLigatureFeatures = features; + onTextHintsChanged(); + } +} + +Uint32 UIConsole::getLigatureFeatures() const { + return mLigaturesOverride ? mLigatureFeatures + : getDefaultTextHints() & TextHints::OpenTypeFeatures; +} + +void UIConsole::clearLigaturesOverride() { + if ( mLigaturesOverride ) { + mLigaturesOverride = false; + onTextHintsChanged(); + } +} + +Uint32 UIConsole::getTextHints() const { + return TextHints::NoKerning | getLigatureFeatures(); +} + +void UIConsole::onTextHintsChanged() { + const Uint32 textHints = getTextHints(); + for ( auto& cache : mTextCache ) + cache.text.setTextHints( textHints ); + invalidateDraw(); +} + Font* UIConsole::getFont() const { return mFontStyleConfig.Font; } @@ -491,6 +524,13 @@ void UIConsole::draw() { Primitives p; p.setColor( Color( mFontStyleConfig.FontSelectionBackColor ).blendAlpha( (Uint8)mAlpha ) ); + const auto characterPos = [this]( const String& string, std::size_t index ) { + return Text::findCharacterPos( index, mFontStyleConfig.Font, mFontStyleConfig.CharacterSize, + string, mFontStyleConfig.Style, 4, + mFontStyleConfig.OutlineThickness, {}, false, + getTextHints() ) + .x; + }; auto to = eemax( mCon.min - mCon.modif, 0 ); auto from = eemin( mCon.max - mCon.modif, (int)mCmdLog.size() - 1 ); @@ -505,23 +545,14 @@ void UIConsole::draw() { auto endCol = eemin( (Int64)mCmdLog[i].log.size(), selNorm.end().column() ); if ( i == selNorm.start().line() ) { - auto tsubstr = mCmdLog[i].log.view().substr( - startCol, selNorm.end().line() == i ? eemax( (Int64)0, endCol - startCol ) - : (Int64)mCmdLog[i].log.size() - startCol ); - auto twidth = - Text::getTextWidth( mFontStyleConfig.Font, mFontStyleConfig.CharacterSize, - tsubstr, mFontStyleConfig.Style ); - auto fsubstr = mCmdLog[i].log.view().substr( 0, startCol ); - auto fwidth = - Text::getTextWidth( mFontStyleConfig.Font, mFontStyleConfig.CharacterSize, - fsubstr, mFontStyleConfig.Style ); + const Int64 selectionEnd = + selNorm.end().line() == i ? endCol : mCmdLog[i].log.size(); + const Float fwidth = characterPos( mCmdLog[i].log, startCol ); + const Float twidth = characterPos( mCmdLog[i].log, selectionEnd ) - fwidth; p.drawRectangle( Rectf( { mScreenPos.x + mPaddingPx.Left + fwidth, curY }, { twidth, lineHeight } ) ); } else if ( i == selNorm.end().line() ) { - auto fsubstr = mCmdLog[i].log.view().substr( 0, endCol ); - auto fwidth = - Text::getTextWidth( mFontStyleConfig.Font, mFontStyleConfig.CharacterSize, - fsubstr, mFontStyleConfig.Style ); + const Float fwidth = characterPos( mCmdLog[i].log, endCol ); p.drawRectangle( Rectf( { mScreenPos.x + mPaddingPx.Left, curY }, { fwidth, lineHeight } ) ); } else { @@ -534,14 +565,26 @@ void UIConsole::draw() { } Text& text = mTextCache[pos].text; + text.setTextHints( getTextHints() ); text.setStyleConfig( mFontStyleConfig ); text.setFillColor( fontColor ); if ( mCmdLog[i].hash != mTextCache[pos].hash ) { - if ( mCmdLog[i].log.size() * cw <= mSize.getWidth() ) { + std::size_t visibleCharacters = mCmdLog[i].log.size(); + if ( getLigatureFeatures() != 0 ) { + visibleCharacters = eemax( + 0, Text::findCharacterFromPos( + { static_cast( mSize.getWidth() + 8 * cw ), 0 }, true, + mFontStyleConfig.Font, mFontStyleConfig.CharacterSize, mCmdLog[i].log, + mFontStyleConfig.Style, 4, mFontStyleConfig.OutlineThickness, {}, + getTextHints() ) ); + } else if ( mCmdLog[i].log.size() * cw > mSize.getWidth() ) { + visibleCharacters = ( mSize.getWidth() + 8 * cw ) / cw; + } + if ( visibleCharacters >= mCmdLog[i].log.size() ) { text.setString( mCmdLog[i].log ); mTextCache[pos].hash = mCmdLog[i].hash; } else { - auto substr = mCmdLog[i].log.substr( 0, ( mSize.getWidth() + 8 * cw ) / cw ); + auto substr = mCmdLog[i].log.substr( 0, visibleCharacters ); mTextCache[pos].hash = String::hash( substr ); text.setString( substr ); } @@ -552,19 +595,22 @@ void UIConsole::draw() { curY = mScreenPos.y + getPixelsSize().getHeight() - mPaddingPx.Bottom - lineHeight - 1; - auto editCharWidth = Text::getTextWidth( String( "> " ), mFontStyleConfig ); + auto editCharWidth = Text::getTextWidth( String( "> " ), mFontStyleConfig, 4, getTextHints() ); + const String& inputLine = mDoc.getCurrentLine().getText(); if ( mDoc.hasSelection() ) { - Float selStartPos = - editCharWidth + Text::getTextWidth( mDoc.getCurrentLine().getText().view().substr( - 0, mDoc.getSelection( true ).start().column() ), - mFontStyleConfig ); - Float selWidth = Text::getTextWidth( mDoc.getSelectedText(), mFontStyleConfig ); + const Float selectionStart = + characterPos( inputLine, mDoc.getSelection( true ).start().column() ); + const Float selectionEnd = + characterPos( inputLine, mDoc.getSelection( true ).end().column() ); + Float selStartPos = editCharWidth + selectionStart; + Float selWidth = selectionEnd - selectionStart; p.drawRectangle( Rectf( { mScreenPos.x + mPaddingPx.Left + selStartPos, curY }, { selWidth, lineHeight } ) ); } Text& text = mTextCache[mTextCache.size() - 1].text; + text.setTextHints( getTextHints() ); text.setStyleConfig( mFontStyleConfig ); text.setFillColor( fontColor ); text.setString( "> " + mDoc.getCurrentLine().getTextWithoutNewLine() ); @@ -572,9 +618,7 @@ void UIConsole::draw() { if ( mCursorVisible ) { Float cursorPos = - editCharWidth + Text::getTextWidth( mDoc.getCurrentLine().getText().view().substr( - 0, mDoc.getSelection().start().column() ), - mFontStyleConfig ); + editCharWidth + characterPos( inputLine, mDoc.getSelection().start().column() ); Rectf r( { mScreenPos.x + mPaddingPx.Left + cursorPos, curY }, { cursorPos, lineHeight } ); updateIMELocation( r ); if ( hasFocus() && getUISceneNode()->getWindow()->getIME().isEditing() ) { @@ -585,6 +629,7 @@ void UIConsole::draw() { Color( fontColor ).blendAlpha( mAlpha ) ); } else { Text& text2 = mTextCache[mTextCache.size() - 2].text; + text2.setTextHints( getTextHints() ); text2.setStyleConfig( mFontStyleConfig ); text2.setFillColor( fontColor ); text2.setString( "_" ); @@ -597,6 +642,7 @@ void UIConsole::draw() { mFontStyleConfig.Font->getGlyph( '_', mFontStyleConfig.CharacterSize, false, false ) .advance; Text& text = mTextCache[mTextCache.size() - 3].text; + text.setTextHints( getTextHints() ); Color OldColor1( text.getColor() ); text.setStyleConfig( mFontStyleConfig ); text.setFillColor( fontColor ); @@ -1103,9 +1149,10 @@ TextPosition UIConsole::getPositionOnScreen( Vector2f position ) { Int64 line = eeclamp( (Int64)eefloor( ( position.y - startOffset ) / lineHeight + 1 ), (Int64)0, (Int64)mCmdLog.size() - 1 ); Int64 fline = eeclamp( firstVisibleLine + line, (Int64)0, (Int64)mCmdLog.size() - 1 ); - Int64 col = Text::findCharacterFromPos( - { (int)eefloor( position.x - mPaddingPx.Left ), 0 }, true, mFontStyleConfig.Font, - mFontStyleConfig.CharacterSize, mCmdLog[fline].log, mFontStyleConfig.Style ); + Int64 col = Text::findCharacterFromPos( { (int)eefloor( position.x - mPaddingPx.Left ), 0 }, + true, mFontStyleConfig.Font, + mFontStyleConfig.CharacterSize, mCmdLog[fline].log, + mFontStyleConfig.Style, 4, 0.f, {}, getTextHints() ); return { fline, col }; } diff --git a/src/eepp/ui/uihtmlimage.cpp b/src/eepp/ui/uihtmlimage.cpp index 9b77dac29..d1d01180b 100644 --- a/src/eepp/ui/uihtmlimage.cpp +++ b/src/eepp/ui/uihtmlimage.cpp @@ -110,14 +110,15 @@ void UIHTMLImage::draw() { break; } style.FontColor = { color.r, color.g, color.b, static_cast( mAlpha ) }; - Float width = Text::getTextWidth( mAlt, style ); + const Uint32 textHints = getDefaultTextHints(); + Float width = Text::getTextWidth( mAlt, style, 4, textHints ); Float available = mSize.x - mPaddingPx.Left - mPaddingPx.Right; Float x = mScreenPos.x + mPaddingPx.Left + eemax( 0.f, ( available - width ) * 0.5f ); Float y = mScreenPos.y + mPaddingPx.Top + ( mSize.y - mPaddingPx.Top - mPaddingPx.Bottom - PixelDensity::getPixelDensity() * style.CharacterSize ) * 0.5f; - Text::draw( String( mAlt ), { x, y }, style ); + Text::draw( String( mAlt ), { x, y }, style, 4, textHints ); } void UIHTMLImage::setAlpha( const Float& alpha ) { diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 4ecf6a7a8..db9e4d0bb 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -446,6 +446,7 @@ UIRichText::UIRichText( const std::string& tag ) : UIHTMLWidget( tag ) { mRichText.getFontStyleConfig().FontColor = Color::Black; mRichText.setTabWidth( mTabSize ); + mRichText.setTextHints( getTextHints() ); setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); } @@ -454,6 +455,32 @@ const RichText& UIRichText::getRichText() { return mRichText; } +void UIRichText::setTextHintsOverride( Uint32 value, Uint32 mask ) { + mask &= TextHints::OpenTypeFeatures; + value &= mask; + if ( mTextHintsOverride != value || mTextHintsOverrideMask != mask ) { + mTextHintsOverride = value; + mTextHintsOverrideMask = mask; + onTextHintsChanged(); + } +} + +void UIRichText::clearTextHintsOverride() { + setTextHintsOverride( 0, 0 ); +} + +Uint32 UIRichText::getTextHints() const { + return UISceneNode::resolveTextHints( getDefaultTextHints(), mTextHintsOverride, + mTextHintsOverrideMask ); +} + +void UIRichText::onTextHintsChanged() { + mRichText.setTextHints( getTextHints() ); + notifyLayoutAttrChange( LayoutInvalidation::TextFormatting ); + notifyLayoutAttrChangeParent( LayoutInvalidation::ParentReplacedFormatting ); + invalidateDraw(); +} + void UIRichText::draw() { if ( mVisible && 0.f != mAlpha ) { UIWidget::draw(); diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 55a9cee64..284118ad2 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -317,6 +317,7 @@ void UISceneNode::initializeEmbeddedFromHost( UISceneNode* hostScene ) { mThreadPool = hostScene->getThreadPool(); mColorSchemePreference = hostScene->getColorSchemePreference(); mContrastPreference = hostScene->getContrastPreference(); + mDefaultTextHints = hostScene->getDefaultTextHints(); const FontService& hostFontService = hostScene->getResourceScope()->getFontService(); FontService& fontService = mResourceScope->getFontService(); fontService.setHinting( hostFontService.getHinting() ); @@ -343,6 +344,37 @@ const std::vector& UISceneNode::getChildUISceneNodes() const { return mChildUISceneNodes; } +void UISceneNode::setDefaultTextHints( Uint32 textHints ) { + textHints &= TextHints::OpenTypeFeatures; + if ( mDefaultTextHints == textHints ) + return; + mDefaultTextHints = textHints; + const auto notifyTextHintsChanged = []( auto&& self, Node* node ) -> void { + if ( node->isType( UI_TYPE_WIDGET ) ) { + UIWidget* widget = static_cast( node ); + widget->onTextHintsChanged(); + if ( widget->getTooltip() ) + widget->getTooltip()->onTextHintsChanged(); + } + for ( Uint32 i = 0; i < node->getChildCount(); ++i ) + self( self, node->getChildAt( i ) ); + }; + notifyTextHintsChanged( notifyTextHintsChanged, this ); + for ( auto* sceneNode : mChildUISceneNodes ) + sceneNode->setDefaultTextHints( textHints ); +} + +Uint32 UISceneNode::getDefaultTextHints() const { + return mDefaultTextHints; +} + +Uint32 UISceneNode::resolveTextHints( Uint32 defaultHints, Uint32 overrideValue, + Uint32 overrideMask ) { + const Uint32 featureMask = TextHints::OpenTypeFeatures; + overrideMask &= featureMask; + return ( defaultHints & featureMask & ~overrideMask ) | ( overrideValue & overrideMask ); +} + void UISceneNode::setHighlightOverRecursive( bool highlight ) { setHighlightOver( highlight ); diff --git a/src/eepp/ui/uitextinput.cpp b/src/eepp/ui/uitextinput.cpp index 5f4167422..d7cddf2d2 100644 --- a/src/eepp/ui/uitextinput.cpp +++ b/src/eepp/ui/uitextinput.cpp @@ -41,6 +41,7 @@ UITextInput::UITextInput( const std::string& tag ) : mMouseDown( false ), mKeyBindings( getInput() ) { mHintCache = Text::New(); + mHintCache->setTextHints( getTextHints() ); UITheme* theme = getUISceneNode()->getUIThemeManager()->getDefaultTheme(); @@ -321,6 +322,7 @@ UITextInput* UITextInput::setMode( TextInputMode mode ) { if ( mMode == TextInputMode::Password ) { if ( !mPassCache ) { mPassCache = Text::New(); + mPassCache->setTextHints( getTextHints() ); updateFontStyleConfig(); } updatePass(); @@ -330,6 +332,14 @@ UITextInput* UITextInput::setMode( TextInputMode mode ) { return this; } +void UITextInput::onTextHintsChanged() { + UITextView::onTextHintsChanged(); + if ( mHintCache ) + mHintCache->setTextHints( getTextHints() ); + if ( mPassCache ) + mPassCache->setTextHints( getTextHints() ); +} + UITextInput::TextInputMode UITextInput::getMode() const { return mMode; } diff --git a/src/eepp/ui/uitextnode.cpp b/src/eepp/ui/uitextnode.cpp index 45d197e34..5b14ca7ed 100644 --- a/src/eepp/ui/uitextnode.cpp +++ b/src/eepp/ui/uitextnode.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -36,6 +37,7 @@ void UITextNode::draw() { parent && parent->isType( UI_TYPE_HTML_WIDGET ) && parent->asType()->isFlex(); if ( isFlexItem ) { Text* flexText = getFlexText(); + flexText->setTextHints( getTextHints() ); if ( flexText->getFont() ) { flexText->setMaxWrapWidth( getPixelsSize().getWidth() ); Float alpha = getAlpha(); @@ -53,7 +55,7 @@ void UITextNode::draw() { Float alpha = getAlpha(); if ( alpha < 1.f ) fc.FontColor.a = (Uint8)( (Float)fc.FontColor.a * alpha ); - Text::draw( mText, mScreenPos.trunc(), fc ); + Text::draw( mText, mScreenPos.trunc(), fc, 4, getTextHints() ); } break; } @@ -129,9 +131,37 @@ Float UITextNode::getBaseline() const { } Text* UITextNode::getFlexText() { - if ( mFlexText == nullptr ) + if ( mFlexText == nullptr ) { mFlexText = Text::New(); + mFlexText->setTextHints( getTextHints() ); + } return mFlexText; } +void UITextNode::setTextHintsOverride( Uint32 value, Uint32 mask ) { + mask &= TextHints::OpenTypeFeatures; + value &= mask; + if ( mTextHintsOverride != value || mTextHintsOverrideMask != mask ) { + mTextHintsOverride = value; + mTextHintsOverrideMask = mask; + onTextHintsChanged(); + } +} + +void UITextNode::clearTextHintsOverride() { + setTextHintsOverride( 0, 0 ); +} + +Uint32 UITextNode::getTextHints() const { + return UISceneNode::resolveTextHints( getDefaultTextHints(), mTextHintsOverride, + mTextHintsOverrideMask ); +} + +void UITextNode::onTextHintsChanged() { + if ( mFlexText ) + mFlexText->setTextHints( getTextHints() ); + notifyLayoutAttrChange( LayoutInvalidation::TextFormatting ); + invalidateDraw(); +} + }} // namespace EE::UI diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 72e8597a4..5cf4a0eac 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -243,7 +243,8 @@ const String& UITextView::getText() const { UITextView* UITextView::setText( const String& text ) { if ( mString != text ) { mString = text; - mTextDrawHints = mString.getTextHints(); + mTextDrawHints = mString.getTextHints() | getTextHints(); + mTextCache.setTextHints( getTextHints() ); mTextCache.setString( mString ); recalculate(); @@ -257,7 +258,8 @@ UITextView* UITextView::setText( const String& text ) { UITextView* UITextView::setText( String&& text ) { if ( mString != text ) { mString = std::move( text ); - mTextDrawHints = mString.getTextHints(); + mTextDrawHints = mString.getTextHints() | getTextHints(); + mTextCache.setTextHints( getTextHints() ); mTextCache.setString( mString ); recalculate(); @@ -268,6 +270,33 @@ UITextView* UITextView::setText( String&& text ) { return this; } +void UITextView::setTextHintsOverride( Uint32 value, Uint32 mask ) { + mask &= TextHints::OpenTypeFeatures; + value &= mask; + if ( mTextHintsOverride != value || mTextHintsOverrideMask != mask ) { + mTextHintsOverride = value; + mTextHintsOverrideMask = mask; + onTextHintsChanged(); + } +} + +void UITextView::clearTextHintsOverride() { + setTextHintsOverride( 0, 0 ); +} + +Uint32 UITextView::getTextHints() const { + return UISceneNode::resolveTextHints( getDefaultTextHints(), mTextHintsOverride, + mTextHintsOverrideMask ); +} + +void UITextView::onTextHintsChanged() { + mTextDrawHints = mString.getTextHints() | getTextHints(); + mTextCache.setTextHints( getTextHints() ); + recalculate(); + notifyLayoutAttrChange( LayoutInvalidation::TextFormatting ); + invalidateDraw(); +} + const Color& UITextView::getFontColor() const { return mFontStyleConfig.FontColor; } diff --git a/src/eepp/ui/uitooltip.cpp b/src/eepp/ui/uitooltip.cpp index 6dd12d4f3..19402a238 100644 --- a/src/eepp/ui/uitooltip.cpp +++ b/src/eepp/ui/uitooltip.cpp @@ -63,6 +63,7 @@ UITooltip::UITooltip() : UIWidget( "tooltip" ), mAlignOffset( 0.f, 0.f ), mTooltipTime( Time::Zero ), mTooltipOf() { mTextCache = Text::New(); + mTextCache->setTextHints( getTextHints() ); mEnabled = false; setFlags( UI_NODE_DEFAULT_FLAGS_CENTERED | UI_AUTO_PADDING | UI_AUTO_SIZE ); @@ -712,4 +713,30 @@ bool UITooltip::isWordWrap() const { return mFlags & UI_WORD_WRAP; } +void UITooltip::setTextHintsOverride( Uint32 value, Uint32 mask ) { + mask &= TextHints::OpenTypeFeatures; + value &= mask; + if ( mTextHintsOverride != value || mTextHintsOverrideMask != mask ) { + mTextHintsOverride = value; + mTextHintsOverrideMask = mask; + onTextHintsChanged(); + } +} + +void UITooltip::clearTextHintsOverride() { + setTextHintsOverride( 0, 0 ); +} + +Uint32 UITooltip::getTextHints() const { + return UISceneNode::resolveTextHints( getDefaultTextHints(), mTextHintsOverride, + mTextHintsOverrideMask ); +} + +void UITooltip::onTextHintsChanged() { + if ( mTextCache ) + mTextCache->setTextHints( getTextHints() ); + onAutoSize(); + invalidateDraw(); +} + }} // namespace EE::UI diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 5028d9326..1c7daf5f1 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -27,6 +27,15 @@ using namespace EE::Window; namespace EE { namespace UI { +Uint32 UIWidget::getDefaultTextHints() const { + const UISceneNode* sceneNode = getUISceneNode(); + return sceneNode ? sceneNode->getDefaultTextHints() : 0; +} + +void UIWidget::onTextHintsChanged() { + invalidateDraw(); +} + static bool isDataAttributeName( std::string_view name ) { return String::istartsWith( String::trim( name ), "data-" ); } diff --git a/src/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index 506ac2505..edb86fae2 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -23,11 +23,15 @@ #include #include #include +#include #include +#include #include #include +#include #include #include +#include #include using namespace EE; @@ -335,6 +339,159 @@ UTEST( FontRendering, destroyingFontInvalidatesTextLayoutCache ) { EXPECT_FALSE( retainedLayoutWeak.expired() ); } +UTEST( FontRendering, fontFeaturesStringConversion ) { + const Uint32 allFeatures = TextHints::StandardLigatures | TextHints::ContextualAlternates | + TextHints::ContextualLigatures | TextHints::DiscretionaryLigatures; + EXPECT_EQ( allFeatures, + Text::fontFeaturesFromString( "'liga', CALT, \"clig\", dlig, unsupported" ) ); + EXPECT_TRUE( Text::fontFeaturesToString( allFeatures ) == "liga,calt,clig,dlig" ); + EXPECT_EQ( 0u, Text::fontFeaturesFromString( "" ) ); + EXPECT_TRUE( Text::fontFeaturesToString( 1u << 31 ).empty() ); +} + +#ifdef EE_TEXT_SHAPER_ENABLED +UTEST( FontRendering, latinOpenTypeFeaturesAreExplicitAndCachedByTextHints ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - Latin Ligatures Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + ResourceScope& scope = *app.getUI()->getResourceScope(); + FontTrueTypePtr font = FontTrueType::New( "LatinLigatures-Regular", scope ); + ASSERT_TRUE( + font->loadFromFile( Sys::getProcessPath() + "../assets/fonts/NotoSans-Regular.ttf" ) ); + + const String text( "fi" ); + const Uint32 latinHints = text.getTextHints(); + ASSERT_TRUE( latinHints & TextHints::AllLatin1 ); + EXPECT_TRUE( Text::canSkipShaping( latinHints ) ); + EXPECT_FALSE( Text::canSkipShaping( latinHints | TextHints::StandardLigatures ) ); + EXPECT_FALSE( Text::canSkipShaping( latinHints | TextHints::ContextualAlternates ) ); + + TextLayout::Cache unshaped = + TextLayout::layout( text, font.get(), 24, Text::Regular, 4, 0, {}, latinHints ); + { + BoolScopedOp shapingOptimizations( Text::TextShaperOptimizations, false ); + TextLayout::Cache shapedWithoutLigatures = + TextLayout::layout( text, font.get(), 24, Text::Regular, 4, 0, {}, latinHints ); + ASSERT_EQ( 1u, shapedWithoutLigatures->paragraphs.size() ); + EXPECT_EQ( 2u, shapedWithoutLigatures->paragraphs.front().shapedGlyphs.size() ); + } + TextLayout::Cache standardLigatures = TextLayout::layout( + text, font.get(), 24, Text::Regular, 4, 0, {}, latinHints | TextHints::StandardLigatures ); + TextLayout::Cache contextualAlternates = + TextLayout::layout( text, font.get(), 24, Text::Regular, 4, 0, {}, + latinHints | TextHints::ContextualAlternates ); + ASSERT_EQ( 1u, unshaped->paragraphs.size() ); + ASSERT_EQ( 1u, standardLigatures->paragraphs.size() ); + ASSERT_EQ( 1u, contextualAlternates->paragraphs.size() ); + EXPECT_EQ( 2u, unshaped->paragraphs.front().shapedGlyphs.size() ); + EXPECT_EQ( 1u, standardLigatures->paragraphs.front().shapedGlyphs.size() ); + EXPECT_EQ( 2u, contextualAlternates->paragraphs.front().shapedGlyphs.size() ); + + TextLayout::Cache cachedStandardLigatures = TextLayout::layout( + text, font.get(), 24, Text::Regular, 4, 0, {}, latinHints | TextHints::StandardLigatures ); + EXPECT_EQ( standardLigatures.get(), cachedStandardLigatures.get() ); + EXPECT_NE( standardLigatures.get(), contextualAlternates.get() ); + TextLayout::Cache noKerningStandardLigatures = + TextLayout::layout( text, font.get(), 24, Text::Regular, 4, 0, {}, + latinHints | TextHints::StandardLigatures | TextHints::NoKerning ); + EXPECT_NE( standardLigatures.get(), noKerningStandardLigatures.get() ); + + const Uint32 ligatureHints = latinHints | TextHints::StandardLigatures; + const Vector2f beforeLigature = Text::findCharacterPos( 0, font.get(), 24, text, Text::Regular, + 4, 0, {}, false, ligatureHints ); + const Vector2f insideLigature = Text::findCharacterPos( 1, font.get(), 24, text, Text::Regular, + 4, 0, {}, false, ligatureHints ); + const Vector2f afterLigature = Text::findCharacterPos( 2, font.get(), 24, text, Text::Regular, + 4, 0, {}, false, ligatureHints ); + EXPECT_LT( beforeLigature.x, insideLigature.x ); + EXPECT_LT( insideLigature.x, afterLigature.x ); + EXPECT_EQ( 1, Text::findCharacterFromPos( insideLigature.asInt(), true, font.get(), 24, text, + Text::Regular, 4, 0, {}, ligatureHints ) ); +} + +UTEST( FontRendering, codeEditorUsesSelectedLigatureFeatures ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - Monospace Ligature Positioning Test", + WindowStyle::Default, WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + ResourceScope& scope = *app.getUI()->getResourceScope(); + FontTrueTypePtr font = FontTrueType::New( "MonospaceLigatures-Regular", scope ); + ASSERT_TRUE( + font->loadFromFile( Sys::getProcessPath() + "../assets/fonts/DejaVuSansMono.ttf" ) ); + ASSERT_TRUE( font->isMonospace() ); + + auto* editor = UICodeEditor::New(); + editor->setParent( app.getUI() ); + editor->setFont( font.get() ); + editor->setFontSize( 24 ); + editor->getDocument().textInput( "fi" ); + editor->setLigatureFeatures( TextHints::ContextualAlternates ); + + const Vector2d editorOffset = editor->getTextPositionOffset( { 0, 2 } ); + const Vector2f shapedOffset = Text::findCharacterPos( + 2, font.get(), editor->getCharacterSize(), editor->getDocument().line( 0 ).getText(), + Text::Regular, editor->getTabWidth(), 0.f, {}, false, + editor->getDocument().line( 0 ).getTextHints() | TextHints::ContextualAlternates | + TextHints::NoKerning ); + EXPECT_NEAR( shapedOffset.x, editorOffset.x, 0.01 ); +} + +UTEST( FontRendering, sceneTextHintsPropagateAndWidgetsCanOverrideThem ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - Scene Text Hints Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + UISceneNode* scene = app.getUI(); + + Text text; + text.setTextHints( TextHints::StandardLigatures ); + text.setString( "fi" ); + EXPECT_TRUE( text.getTextHints() & TextHints::StandardLigatures ); + + auto* textView = UITextView::New(); + textView->setParent( scene ); + auto* tooltip = textView->createTooltip(); + auto* richText = UIRichText::New(); + richText->setParent( scene ); + auto* textNode = UITextNode::New(); + textNode->setParent( scene ); + auto* console = UIConsole::NewOpt( nullptr, false, false ); + console->setParent( scene ); + auto* editor = UICodeEditor::New(); + editor->setParent( scene ); + + scene->setDefaultTextHints( TextHints::StandardLigatures | TextHints::ContextualAlternates ); + EXPECT_TRUE( textView->getTextCache()->getTextHints() & TextHints::StandardLigatures ); + EXPECT_TRUE( tooltip->getTextCache()->getTextHints() & TextHints::StandardLigatures ); + EXPECT_TRUE( richText->getRichText().getTextHints() & TextHints::StandardLigatures ); + EXPECT_TRUE( textNode->getTextHints() & TextHints::StandardLigatures ); + EXPECT_EQ( + static_cast( TextHints::StandardLigatures | TextHints::ContextualAlternates ), + console->getLigatureFeatures() ); + EXPECT_EQ( + static_cast( TextHints::StandardLigatures | TextHints::ContextualAlternates ), + editor->getLigatureFeatures() ); + + textView->setTextHintsOverride( 0, TextHints::StandardLigatures ); + EXPECT_FALSE( textView->getTextCache()->getTextHints() & TextHints::StandardLigatures ); + EXPECT_TRUE( textView->getTextCache()->getTextHints() & TextHints::ContextualAlternates ); + editor->setLigatureFeatures( TextHints::StandardLigatures ); + EXPECT_EQ( static_cast( TextHints::StandardLigatures ), editor->getLigatureFeatures() ); + editor->clearLigaturesOverride(); + EXPECT_EQ( + static_cast( TextHints::StandardLigatures | TextHints::ContextualAlternates ), + editor->getLigatureFeatures() ); + console->setLigatureFeatures( TextHints::DiscretionaryLigatures ); + EXPECT_EQ( static_cast( TextHints::DiscretionaryLigatures ), + console->getLigatureFeatures() ); + console->clearLigaturesOverride(); + EXPECT_EQ( + static_cast( TextHints::StandardLigatures | TextHints::ContextualAlternates ), + console->getLigatureFeatures() ); +} +#endif + UTEST( FontRendering, fontsTest ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 580ea7114..3b8a841a6 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -145,6 +145,8 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, editor.horizontalScrollbar = ini.getValueB( "editor", "horizontal_scrollbar", true ); editor.openDocumentsInMainSplit = ini.getValueB( "editor", "open_documents_in_main_split", false ); + editor.fontFeatures = + Graphics::Text::fontFeaturesFromString( ini.getValue( "editor", "font_features" ) ); ui.fontSize = ini.getValue( "ui", "font_size", "11dp" ); ui.panelFontSize = ini.getValue( "ui", "panel_font_size", "11dp" ); ui.showSidePanel = ini.getValueB( "ui", "show_side_panel", true ); @@ -169,6 +171,8 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, FontTrueType::fontHintingFromString( ini.getValue( "ui", "font_hinting", "full" ) ); ui.fontAntialiasing = FontTrueType::fontAntialiasingFromString( ini.getValue( "ui", "font_antialiasing", "grayscale" ) ); + ui.fontFeatures = + Graphics::Text::fontFeaturesFromString( ini.getValue( "ui", "font_features" ) ); ui.editorFontInInputFields = ini.getValueB( "ui", "editor_font_in_input_fields", true ); doc.trimTrailingWhitespaces = ini.getValueB( "document", "trim_trailing_whitespaces", false ); @@ -350,6 +354,8 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "editor", "vertical_scrollbar", editor.verticalScrollbar ); ini.setValueB( "editor", "horizontal_scrollbar", editor.horizontalScrollbar ); ini.setValueB( "editor", "open_documents_in_main_split", editor.openDocumentsInMainSplit ); + ini.setValue( "editor", "font_features", + Graphics::Text::fontFeaturesToString( editor.fontFeatures ) ); ini.setValue( "editor", "font_size", editor.fontSize.toString() ); ini.setValue( "ui", "font_size", ui.fontSize.toString() ); @@ -374,6 +380,7 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValue( "ui", "font_hinting", FontTrueType::fontHintingToString( ui.fontHinting ) ); ini.setValue( "ui", "font_antialiasing", FontTrueType::fontAntialiasingToString( ui.fontAntialiasing ) ); + ini.setValue( "ui", "font_features", Graphics::Text::fontFeaturesToString( ui.fontFeatures ) ); ini.setValue( "screenshots", "save_path", screenshot.savePath ); ini.setValue( "screenshots", "filename_pattern", screenshot.filenamePattern ); ini.setValue( "screenshots", "save_format", screenshot.saveFormat ); diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index e9d653289..408d3ab69 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -81,6 +81,7 @@ struct UIConfig { std::string language; FontHinting fontHinting{ FontHinting::Full }; FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale }; + Uint32 fontFeatures{ 0 }; }; struct WindowStateConfig { @@ -107,6 +108,7 @@ struct CodeEditorConfig { std::string colorScheme{ "ecode" }; StyleSheetLength fontSize{ 11, StyleSheetLength::Dp }; StyleSheetLength lineSpacing{ 0, StyleSheetLength::Dp }; + Uint32 fontFeatures{ 0 }; bool showLineNumbers{ true }; bool showWhiteSpaces{ true }; bool showLineEndings{ false }; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index bbf69bb7d..470737eb8 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -1981,6 +1981,7 @@ void App::setTheme( const std::string& path ) { ->add( theme ); mUISceneNode->setTheme( theme.get() ); + mUISceneNode->setDefaultTextHints( mConfig.ui.fontFeatures ); mUISceneNode->getRoot()->addClass( "appbackground" ); @@ -2986,6 +2987,7 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { editor->setFoldDrawable( findIcon( "chevron-down", PixelDensity::dpToPxI( 12 ) ) ); editor->setFoldedDrawable( findIcon( "chevron-right", PixelDensity::dpToPxI( 12 ) ) ); editor->setTabStops( mConfig.doc.tabStops ); + editor->setLigatureFeatures( mConfig.editor.fontFeatures ); editor->setEnableInlineColorBoxes( config.inlineColorBoxes ); doc.setAutoCloseBrackets( !mConfig.editor.autoCloseBrackets.empty() ); diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index eaaa87906..4db9c51a1 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -3316,6 +3316,64 @@ UIMenu* SettingsMenu::createFontAntiAliasingMenu() { UIMenu* SettingsMenu::createFontsMenu() { mFontsMenu = UIPopUpMenu::New(); + const auto createFontFeaturesMenu = [this]( bool editorFeatures ) { + auto* menu = UIPopUpMenu::New(); + const Uint32 features = editorFeatures ? mApp->getConfig().editor.fontFeatures + : mApp->getConfig().ui.fontFeatures; + menu->addCheckBox( i18n( "standard_ligatures", "Standard Ligatures (liga)" ), + features & TextHints::StandardLigatures ) + ->setTooltipText( + i18n( "standard_ligatures_desc", + "Typographic combinations such as fi, fl, and ffi, depending on the font." ) ) + ->setId( "liga" ); + menu->addCheckBox( i18n( "contextual_alternates", "Contextual Alternates (calt)" ), + features & TextHints::ContextualAlternates ) + ->setTooltipText( + i18n( "contextual_alternates_desc", + "Context-dependent alternatives, including many programming ligatures." ) ) + ->setId( "calt" ); + menu->addCheckBox( i18n( "contextual_ligatures", "Contextual Ligatures (clig)" ), + features & TextHints::ContextualLigatures ) + ->setTooltipText( + i18n( "contextual_ligatures_desc", + "Ligatures applied in specific contexts to improve readability." ) ) + ->setId( "clig" ); + menu->addCheckBox( i18n( "discretionary_ligatures", "Discretionary Ligatures (dlig)" ), + features & TextHints::DiscretionaryLigatures ) + ->setTooltipText( + i18n( "discretionary_ligatures_desc", + "Optional decorative or stylistic ligatures provided by the font." ) ) + ->setId( "dlig" ); + menu->on( Event::OnItemClicked, [this, editorFeatures]( const Event* event ) { + if ( !event->getNode()->isType( UI_TYPE_MENUCHECKBOX ) ) + return; + auto* item = event->getNode()->asType(); + const String& id = item->getId(); + const Uint32 feature = id == "liga" ? TextHints::StandardLigatures + : id == "calt" ? TextHints::ContextualAlternates + : id == "clig" ? TextHints::ContextualLigatures + : id == "dlig" ? TextHints::DiscretionaryLigatures + : 0; + if ( feature == 0 ) + return; + Uint32& features = editorFeatures ? mApp->getConfig().editor.fontFeatures + : mApp->getConfig().ui.fontFeatures; + if ( item->isActive() ) + features |= feature; + else + features &= ~feature; + if ( editorFeatures ) { + mSplitter->forEachEditor( [features]( UICodeEditor* editor ) { + editor->setLigatureFeatures( features ); + } ); + } else { + mApp->getUISceneNode()->setDefaultTextHints( features ); + } + } ); + return menu; + }; + auto* uiFontFeaturesMenu = createFontFeaturesMenu( false ); + auto* editorFontFeaturesMenu = createFontFeaturesMenu( true ); mFontsMenu->addSubMenu( i18n( "ui_font_hint", "Font Hint" ), findIcon( "font-size" ), createFontHintMenu() ); mFontsMenu->addSubMenu( i18n( "ui_font_antialiasing", "Font Anti-Aliasing" ), @@ -3325,10 +3383,14 @@ UIMenu* SettingsMenu::createFontsMenu() { mFontsMenu ->add( i18n( "ui_font_and_size_ellipsis", "UI Font & Size..." ), findIcon( "font-size" ) ) ->setId( "sans-serif-font" ); + mFontsMenu->addSubMenu( i18n( "ui_font_features", "UI Font Features" ), findIcon( "font-size" ), + uiFontFeaturesMenu ); mFontsMenu ->add( i18n( "editor_font_and_size_ellipsis", "Editor Font & Size..." ), findIcon( "font-size" ) ) ->setId( "editor-font" ); + mFontsMenu->addSubMenu( i18n( "editor_font_features", "Editor Font Features" ), + findIcon( "font-size" ), editorFontFeaturesMenu ); mFontsMenu ->add( i18n( "terminal_font_and_size_ellipsis", "Terminal Font & Size..." ), findIcon( "font-size" ) )