mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 02:30:25 +03:00
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:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() );
|
||||
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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() );
|
||||
|
||||
@@ -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" ) )
|
||||
|
||||
Reference in New Issue
Block a user