From ffabb19d604b99602ca5dea7fc006237f670db9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 15 Aug 2026 02:21:11 -0300 Subject: [PATCH] Add configurable OpenType font features to text rendering: Support explicit liga, calt, clig, and dlig shaping features and include text hints in layout caching, measurement, drawing, and character position calculations. Propagate scene-wide font feature defaults through UI text producers, with granular widget overrides and recursive cache invalidation. Update UICodeEditor and UIConsole to use shaped positioning when font features are enabled, including with monospaced fonts. Add separate UI and editor font feature settings to ecode, persist them as comma-separated OpenType tags, and apply changes immediately. Add localized menu labels, reusable Text feature conversion helpers, and regression coverage for shaping, positioning, propagation, and parsing. --- bin/assets/i18n/de.xml | 10 ++ bin/assets/i18n/en.xml | 10 ++ bin/assets/i18n/fr.xml | 10 ++ bin/assets/i18n/zh.xml | 10 ++ include/eepp/core/string.hpp | 6 + include/eepp/graphics/richtext.hpp | 5 + include/eepp/graphics/text.hpp | 7 +- include/eepp/ui/doc/documentview.hpp | 9 +- include/eepp/ui/uicodeeditor.hpp | 16 +- include/eepp/ui/uiconsole.hpp | 8 + include/eepp/ui/uirichtext.hpp | 10 ++ include/eepp/ui/uiscenenode.hpp | 9 ++ include/eepp/ui/uitextinput.hpp | 2 + include/eepp/ui/uitextnode.hpp | 7 + include/eepp/ui/uitextview.hpp | 10 ++ include/eepp/ui/uitooltip.hpp | 10 ++ include/eepp/ui/uiwidget.hpp | 6 + src/eepp/graphics/richtext.cpp | 21 +++ src/eepp/graphics/text.cpp | 137 ++++++++++++++-- src/eepp/graphics/textlayout.cpp | 37 +++-- src/eepp/ui/doc/documentview.cpp | 47 +++--- src/eepp/ui/tools/uidiffview.cpp | 8 +- src/eepp/ui/uicodeeditor.cpp | 78 ++++++--- src/eepp/ui/uiconsole.cpp | 103 ++++++++---- src/eepp/ui/uihtmlimage.cpp | 5 +- src/eepp/ui/uirichtext.cpp | 27 ++++ src/eepp/ui/uiscenenode.cpp | 32 ++++ src/eepp/ui/uitextinput.cpp | 10 ++ src/eepp/ui/uitextnode.cpp | 34 +++- src/eepp/ui/uitextview.cpp | 33 +++- src/eepp/ui/uitooltip.cpp | 27 ++++ src/eepp/ui/uiwidget.cpp | 9 ++ src/tests/unit_tests/fontrendering_tests.cpp | 157 +++++++++++++++++++ src/tools/ecode/appconfig.cpp | 7 + src/tools/ecode/appconfig.hpp | 2 + src/tools/ecode/ecode.cpp | 2 + src/tools/ecode/settingsmenu.cpp | 62 ++++++++ 37 files changed, 882 insertions(+), 101 deletions(-) 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" ) )