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.
This commit is contained in:
Martín Lucas Golini
2026-08-15 02:21:11 -03:00
parent c1034fa87e
commit ffabb19d60
37 changed files with 882 additions and 101 deletions
+21
View File
@@ -502,6 +502,17 @@ static void setInlineItemTextTabWidth( std::vector<RichText::InlineItem>& items,
}
}
static void setInlineItemTextHints( std::vector<RichText::InlineItem>& 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<TextType>( 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<FloatExclusion>& exclusions ) {
if ( mExternalFloatExclusions == exclusions )
return false;
+127 -10
View File
@@ -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<Float> 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<Float>( boundaryIndex ) / static_cast<Float>( 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<Uint32>::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<std::size_t>(
static_cast<std::size_t>( 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::size_t>(
std::round( ratio * static_cast<Float>( 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,
+26 -11
View File
@@ -149,7 +149,7 @@ static void segmentString( TextLayout& result, String::View input, Callable cb,
template <typename Callable>
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<std::uint32_t>( 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<Float> tabOffset, TextDirection direction,
LineWrapMode wrapMode, Uint32 wrapWidth, bool keepIndentation,
std::optional<Float> tabOffset, Uint32 textDrawHints,
TextDirection direction, LineWrapMode wrapMode,
Uint32 wrapWidth, bool keepIndentation,
Float initialXOffset ) {
return hashCombine( std::hash<String::View>()( string ), std::hash<Font*>()( font ),
std::hash<Uint32>()( characterSize ), std::hash<Uint32>()( style ),
std::hash<Uint32>()( tabWidth ), std::hash<Float>()( outlineThickness ),
std::hash<std::optional<Float>>()( tabOffset ),
std::hash<Uint32>()( textDrawHints & ( TextHints::NoKerning |
TextHints::OpenTypeFeatures ) ),
std::hash<std::underlying_type_t<TextDirection>>()(
static_cast<std::underlying_type_t<TextDirection>>( direction ) ),
std::hash<std::underlying_type_t<LineWrapMode>>()(
@@ -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<FontTrueType*>( 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 ) {
+27 -20
View File
@@ -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<TextDocument> 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<Int64>( toVisibleIndex( toLine, true ) );
if ( oldIdxFrom == static_cast<Int64>( VisibleIndex::invalid ) ||
oldIdxTo == static_cast<Int64>( VisibleIndex::invalid ) ||
oldIdxFrom > oldIdxTo ||
oldIdxFrom >= (Int64)mVisibleLines.size() ||
oldIdxTo >= (Int64)mVisibleLines.size() ) {
oldIdxTo == static_cast<Int64>( 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<Int64>( 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<Int64>( VisibleIndex::invalid )
: visibleIdx;
isFolded( visibleLine.line(), true )
? static_cast<Int64>( 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<Int64>( oldIdxFromVI );
Int64 oldIdxTo = static_cast<Int64>( 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
+5 -3
View File
@@ -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:
+59 -19
View File
@@ -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<TextDocument>() ),
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<FontTrueType*>( mFont )->isIdentifiedAsMonospace() &&
mDoc->line( lineIndex ).isAscii() ) );
return getLigatureFeatures() == 0 && mFont &&
( ( mFont->isMonospace() &&
( !Text::TextShaperEnabled || mDoc->line( lineIndex ).isAscii() ) ) ||
( mFont->getType() == FontType::TTF &&
static_cast<FontTrueType*>( 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;
+75 -28
View File
@@ -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<Int32>(
0, Text::findCharacterFromPos(
{ static_cast<Int32>( 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 };
}
+3 -2
View File
@@ -110,14 +110,15 @@ void UIHTMLImage::draw() {
break;
}
style.FontColor = { color.r, color.g, color.b, static_cast<Uint8>( 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 ) {
+27
View File
@@ -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();
+32
View File
@@ -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*>& 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<UIWidget*>( 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 );
+10
View File
@@ -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;
}
+32 -2
View File
@@ -1,5 +1,6 @@
#include <eepp/graphics/text.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uitextnode.hpp>
@@ -36,6 +37,7 @@ void UITextNode::draw() {
parent && parent->isType( UI_TYPE_HTML_WIDGET ) && parent->asType<UIHTMLWidget>()->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
+31 -2
View File
@@ -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;
}
+27
View File
@@ -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
+9
View File
@@ -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-" );
}
@@ -23,11 +23,15 @@
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uicodeeditor.hpp>
#include <eepp/ui/uiconsole.hpp>
#include <eepp/ui/uiicon.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitextedit.hpp>
#include <eepp/ui/uitextnode.hpp>
#include <eepp/ui/uitextview.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uitooltip.hpp>
#include <eepp/window/engine.hpp>
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<Uint32>( TextHints::StandardLigatures | TextHints::ContextualAlternates ),
console->getLigatureFeatures() );
EXPECT_EQ(
static_cast<Uint32>( 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<Uint32>( TextHints::StandardLigatures ), editor->getLigatureFeatures() );
editor->clearLigaturesOverride();
EXPECT_EQ(
static_cast<Uint32>( TextHints::StandardLigatures | TextHints::ContextualAlternates ),
editor->getLigatureFeatures() );
console->setLigatureFeatures( TextHints::DiscretionaryLigatures );
EXPECT_EQ( static_cast<Uint32>( TextHints::DiscretionaryLigatures ),
console->getLigatureFeatures() );
console->clearLigaturesOverride();
EXPECT_EQ(
static_cast<Uint32>( TextHints::StandardLigatures | TextHints::ContextualAlternates ),
console->getLigatureFeatures() );
}
#endif
UTEST( FontRendering, fontsTest ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
+7
View File
@@ -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<std::string>& 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<std::string>& 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 );
+2
View File
@@ -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 };
+2
View File
@@ -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() );
+62
View File
@@ -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<UIMenuCheckBox>();
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" ) )