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" ) )