From 427852836c64aec521de5b2b40ec5e5c44590093 Mon Sep 17 00:00:00 2001 From: attila Date: Wed, 5 Mar 2025 18:09:25 +0100 Subject: [PATCH] ShapedText: Break ligatures into multiple iterable placeholder glyphs --- .../detail/juce_JustifiedText.cpp | 55 ++++---- .../juce_graphics/detail/juce_JustifiedText.h | 37 ++++- .../juce_graphics/detail/juce_ShapedText.cpp | 62 +++++++-- .../juce_graphics/detail/juce_ShapedText.h | 10 +- .../detail/juce_SimpleShapedText.cpp | 131 +++++++++++++++--- .../detail/juce_SimpleShapedText.h | 39 +++++- .../fonts/juce_GlyphArrangement.cpp | 3 + .../juce_graphics/fonts/juce_TextLayout.cpp | 15 +- 8 files changed, 282 insertions(+), 70 deletions(-) diff --git a/modules/juce_graphics/detail/juce_JustifiedText.cpp b/modules/juce_graphics/detail/juce_JustifiedText.cpp index b2463b7352..2a4bbb7e28 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.cpp +++ b/modules/juce_graphics/detail/juce_JustifiedText.cpp @@ -221,7 +221,7 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions std::vector lineInfos; - for (const auto [range, lineNumber] : shapedText.getLineNumbers()) + for (const auto [range, lineNumber] : shapedText.getLineNumbersForGlyphRanges()) { // This is guaranteed by the RangedValues implementation. You can't assign a value to an // empty range. @@ -259,7 +259,7 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions const auto containsHardBreak = shapedText.getCodepoint (range.getEnd() - 1) == 0xa || shapedText.getCodepoint (range.getStart()) == 0xa; - if (containsHardBreak || lineNumber == shapedText.getLineNumbers().back().value) + if (containsHardBreak || lineNumber == shapedText.getLineNumbersForGlyphRanges().back().value) { m.extraWhitespaceAdvance = {}; m.stretchableWhitespaces = {}; @@ -282,7 +282,7 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions for (const auto [lineIndex, lineInfo] : enumerate (lineInfos)) { - const auto lineNumber = shapedText.getLineNumbers().getItem ((size_t) lineIndex); + const auto lineNumber = shapedText.getLineNumbersForGlyphRanges().getItem ((size_t) lineIndex); const auto range = lineNumber.range; const auto maxDescent = lineInfo.lineHeight - lineInfo.maxAscent; @@ -291,16 +291,17 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions if (! top.has_value()) top = baseline - (1.0f + leading) * lineInfo.maxAscent; - linesMetrics.set (range, - { lineNumber.value, - { lineInfo.mainAxisLineAlignment.anchor, baseline }, - lineInfo.maxAscent, - lineInfo.lineHeight - lineInfo.maxAscent, - lineInfo.mainAxisLineAlignment.effectiveLineLength + lineInfo.mainAxisLineAlignment.extraWhitespaceAdvance, - *top, - nextLineTop }, - ops, - MergeEqualItemsNo{}); + lineMetricsForGlyphRange.set (range, + { lineNumber.value, + { lineInfo.mainAxisLineAlignment.anchor, baseline }, + lineInfo.maxAscent, + lineInfo.lineHeight - lineInfo.maxAscent, + lineInfo.mainAxisLineAlignment.effectiveLineLength + + lineInfo.mainAxisLineAlignment.extraWhitespaceAdvance, + *top, + nextLineTop }, + ops, + MergeEqualItemsNo{}); whitespaceStretch.set (range, 0.0f, ops); const auto stretchRange = lineInfo.mainAxisLineAlignment.stretchableWhitespaces + range.getStart(); @@ -324,10 +325,10 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions // The remaining logic below is for supporting // GlyphArrangement::addFittedText() when the maximum number of lines is // constrained. - if (linesMetrics.isEmpty()) + if (lineMetricsForGlyphRange.isEmpty()) return; - const auto lastLineMetrics = linesMetrics.back(); + const auto lastLineMetrics = lineMetricsForGlyphRange.back(); const auto lastLineGlyphRange = lastLineMetrics.range; const auto lastLineGlyphs = shapedText.getGlyphs (lastLineGlyphRange); const auto lastLineLengths = getMainAxisLineLength (lastLineGlyphs); @@ -469,12 +470,12 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions realign.extraWhitespaceAdvance, ops); } -int64 JustifiedText::getGlyphIndexAt (Point p) const +int64 JustifiedText::getGlyphIndexToTheRightOf (Point p) const { - auto lineIt = linesMetrics.begin(); + auto lineIt = lineMetricsForGlyphRange.begin(); float lineTop = 0.0f; - while (lineIt != linesMetrics.end()) + while (lineIt != lineMetricsForGlyphRange.end()) { const auto nextLineTop = lineIt->value.nextLineTop; @@ -485,7 +486,7 @@ int64 JustifiedText::getGlyphIndexAt (Point p) const ++lineIt; } - if (lineIt == linesMetrics.end()) + if (lineIt == lineMetricsForGlyphRange.end()) return 0; const auto glyphsInLine = shapedText.getGlyphs (lineIt->range); @@ -495,7 +496,7 @@ int64 JustifiedText::getGlyphIndexAt (Point p) const for (const auto& glyph : glyphsInLine) { - if ( p.getX() <= glyphX + if ( p.getX() < glyphX + glyph.advance.getX() / 2.0f || glyph.isNewline() || (glyphIndex - lineIt->range.getStart() == (int64) glyphsInLine.size() - 1 && glyph.isWhitespace())) { @@ -513,10 +514,12 @@ GlyphAnchorResult JustifiedText::getGlyphAnchor (int64 index) const { jassert (index >= 0); - if (linesMetrics.isEmpty()) + if (lineMetricsForGlyphRange.isEmpty()) return {}; - const auto lineItem = linesMetrics.getItemWithEnclosingRange (index).value_or (linesMetrics.back()); + const auto lineItem = lineMetricsForGlyphRange.getItemWithEnclosingRange (index) + .value_or (lineMetricsForGlyphRange.back()); + const auto indexInLine = index - lineItem.range.getStart(); GlyphAnchorResult anchor { lineItem.value.anchor, lineItem.value.maxAscent, lineItem.value.maxDescent }; @@ -539,7 +542,7 @@ RectangleList JustifiedText::getGlyphsBounds (Range glyphRange) co { RectangleList bounds; - if (linesMetrics.isEmpty()) + if (lineMetricsForGlyphRange.isEmpty()) return bounds; const auto getBounds = [&] (const LineMetrics& line, int64 lineStart, int64 boundsStart, int64 boundsEnd) -> Rectangle @@ -569,7 +572,7 @@ RectangleList JustifiedText::getGlyphsBounds (Range glyphRange) co for (auto consumeFrom = glyphRange.getStart(); consumeFrom < glyphRange.getEnd();) { - const auto lineItem = linesMetrics.getItemWithEnclosingRange (consumeFrom); + const auto lineItem = lineMetricsForGlyphRange.getItemWithEnclosingRange (consumeFrom); if (! lineItem.has_value()) break; @@ -585,10 +588,10 @@ RectangleList JustifiedText::getGlyphsBounds (Range glyphRange) co float JustifiedText::getHeight() const { - if (linesMetrics.isEmpty()) + if (lineMetricsForGlyphRange.isEmpty()) return 0.0f; - return linesMetrics.back().value.nextLineTop; + return lineMetricsForGlyphRange.back().value.nextLineTop; } void drawJustifiedText (const JustifiedText& text, const Graphics& g, AffineTransform transform) diff --git a/modules/juce_graphics/detail/juce_JustifiedText.h b/modules/juce_graphics/detail/juce_JustifiedText.h index fa34ff544b..cf0d6307cc 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.h +++ b/modules/juce_graphics/detail/juce_JustifiedText.h @@ -113,10 +113,11 @@ public: void accessTogetherWith (Callable&& callback, RangedValues&&... rangedValues) const { std::optional lastLine; + int64 lastGlyph = 0; Point anchor {}; for (const auto item : makeIntersectingRangedValues (&shapedText.getResolvedFonts(), - &linesMetrics, + &lineMetricsForGlyphRange, &rangesToDraw, &whitespaceStretch, (&rangedValues)...)) @@ -127,6 +128,34 @@ public: if (std::exchange (lastLine, lineMetrics.lineNumber) != lineMetrics.lineNumber) anchor = lineMetrics.anchor; + if (range.getStart() != lastGlyph) + { + detail::RangedValues glyphMask; + Ranges::Operations ops; + + const auto firstGlyphInCurrentLine = shapedText.getLineNumbersForGlyphRanges().getItem ((size_t) lineMetrics.lineNumber) + .range + .getStart(); + + glyphMask.set ({ std::max (lastGlyph, firstGlyphInCurrentLine), range.getStart() }, 1, ops); + + for (const auto [skippedRange, skippedStretch, _] : makeIntersectingRangedValues (&whitespaceStretch, + &glyphMask)) + { + ignoreUnused (_); + + for (const auto& skippedGlyph : shapedText.getGlyphs (skippedRange)) + { + anchor += skippedGlyph.advance; + + if (skippedGlyph.isWhitespace()) + anchor.addXY (skippedStretch, 0.0f); + } + } + } + + lastGlyph = range.getEnd(); + const auto glyphs = [this, r = range, dt = drawType]() -> Span { if (dt == DrawType::ellipsis) @@ -170,7 +199,7 @@ public: */ auto& getMinimumRequiredWidthForLines() const { return minimumRequiredWidthsForLine; } - int64 getGlyphIndexAt (Point p) const; + int64 getGlyphIndexToTheRightOf (Point p) const; /* If the passed in index parameter is greater than the index of the last contained glyph, then the returned anchor specifies the location where the next glyph would have to be @@ -190,11 +219,11 @@ public: */ float getHeight() const; - const auto& getLinesMetrics() const { return linesMetrics; } + const auto& getLineMetricsForGlyphRange() const { return lineMetricsForGlyphRange; } private: const SimpleShapedText& shapedText; - detail::RangedValues linesMetrics; + detail::RangedValues lineMetricsForGlyphRange; std::optional ellipsis; detail::RangedValues rangesToDraw; detail::RangedValues whitespaceStretch; diff --git a/modules/juce_graphics/detail/juce_ShapedText.cpp b/modules/juce_graphics/detail/juce_ShapedText.cpp index 1a26f5025e..527d27e94d 100644 --- a/modules/juce_graphics/detail/juce_ShapedText.cpp +++ b/modules/juce_graphics/detail/juce_ShapedText.cpp @@ -59,9 +59,14 @@ public: return simpleShapedText.getNumGlyphs(); } - const detail::RangedValues& getLinesMetrics() const + const detail::RangedValues& getLineMetricsForGlyphRange() const { - return justifiedText.getLinesMetrics(); + return justifiedText.getLineMetricsForGlyphRange(); + } + + const detail::Ranges& getLineTextRanges() const + { + return simpleShapedText.getLineTextRanges(); } auto& getText() const @@ -74,9 +79,40 @@ public: return simpleShapedText.getTextRange (glyphIndex); } - int64 getGlyphIndexAt (Point p) const + auto isLtr (int64 glyphIndex) const { - return justifiedText.getGlyphIndexAt (p); + return simpleShapedText.isLtr (glyphIndex); + } + + int64 getTextIndexForCaret (Point p) const + { + const auto getGlyph = [&] (int64 i) + { + return simpleShapedText.getGlyphs()[(size_t) i]; + }; + + if (getNumGlyphs() == 0) + return 0; + + const auto glyphOnTheRight = justifiedText.getGlyphIndexToTheRightOf (p); + + if (glyphOnTheRight >= getNumGlyphs()) + { + const auto glyphOnTheLeft = glyphOnTheRight - 1; + const auto ltr = simpleShapedText.getGlyphLookup().find (getGlyph (glyphOnTheLeft).cluster)->value.ltr; + + if (ltr) + return simpleShapedText.getTextIndexAfterGlyph (glyphOnTheLeft); + + return simpleShapedText.getGlyphs()[(size_t) glyphOnTheLeft].cluster; + } + + const auto ltr = simpleShapedText.getGlyphLookup().find (getGlyph (glyphOnTheRight).cluster)->value.ltr; + + if (ltr) + return simpleShapedText.getGlyphs()[(size_t) glyphOnTheRight].cluster; + + return simpleShapedText.getTextIndexAfterGlyph (glyphOnTheRight); } void getGlyphRanges (Range textRange, std::vector>& outRanges) const @@ -139,9 +175,14 @@ int64 ShapedText::getNumGlyphs() const return impl->getNumGlyphs(); } -const detail::RangedValues& ShapedText::getLinesMetrics() const +const detail::RangedValues& ShapedText::getLineMetricsForGlyphRange() const { - return impl->getLinesMetrics(); + return impl->getLineMetricsForGlyphRange(); +} + +const detail::Ranges& ShapedText::getLineTextRanges() const +{ + return impl->getLineTextRanges(); } const String& ShapedText::getText() const @@ -154,9 +195,14 @@ Range ShapedText::getTextRange (int64 glyphIndex) const return impl->getTextRange (glyphIndex); } -int64 ShapedText::getGlyphIndexAt (Point p) const +bool ShapedText::isLtr (int64 glyphIndex) const { - return impl->getGlyphIndexAt (p); + return impl->isLtr (glyphIndex); +} + +int64 ShapedText::getTextIndexForCaret (Point p) const +{ + return impl->getTextIndexForCaret (p); } void ShapedText::getGlyphRanges (Range textRange, std::vector>& outRanges) const diff --git a/modules/juce_graphics/detail/juce_ShapedText.h b/modules/juce_graphics/detail/juce_ShapedText.h index de69c6c10a..33b66195e4 100644 --- a/modules/juce_graphics/detail/juce_ShapedText.h +++ b/modules/juce_graphics/detail/juce_ShapedText.h @@ -52,6 +52,8 @@ public: /* Returns the text which was used to construct this object. */ const String& getText() const; + Span getGlyphs() const; + /* Returns the text's codepoint range, to which the glyph under the provided index belongs. This range will have a length of at least one, and potentially more than one if ligatures @@ -59,7 +61,9 @@ public: */ Range getTextRange (int64 glyphIndex) const; - int64 getGlyphIndexAt (Point p) const; + bool isLtr (int64 glyphIndex) const; + + int64 getTextIndexForCaret (Point p) const; void getGlyphRanges (Range textRange, std::vector>& outRanges) const; @@ -94,7 +98,9 @@ public: int64 getNumGlyphs() const; - const detail::RangedValues& getLinesMetrics() const; + const detail::RangedValues& getLineMetricsForGlyphRange() const; + + const detail::Ranges& getLineTextRanges() const; /* @internal */ const JustifiedText& getJustifiedText() const; diff --git a/modules/juce_graphics/detail/juce_SimpleShapedText.cpp b/modules/juce_graphics/detail/juce_SimpleShapedText.cpp index ffd5ff5c60..227edd00d3 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.cpp +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.cpp @@ -317,14 +317,28 @@ static std::vector lowLevelShape (const String& string, std::vector characterLookup; std::vector glyphs; - std::optional lastCluster; + std::optional lastCluster; - for (size_t i = 0; i < infos.size(); ++i) + const auto ltr = (embeddingLevel % 2) == 0; + + const auto getNextCluster = [<r, &infosCapt = infos, &range] (size_t visualIndex) { - const auto j = (embeddingLevel % 2) == 0 ? i : infos.size() - 1 - i; + const auto next = (int64) visualIndex + (ltr ? 1 : -1); - const auto glyphId = infos[j].codepoint; - const auto xAdvance = positions[j].x_advance; + if (next < 0) + return ltr ? range.getStart() : range.getEnd(); + + if (next >= (int64) infosCapt.size()) + return ltr ? range.getEnd() : range.getStart(); + + return (int64) infosCapt[(size_t) next].cluster + range.getStart(); + }; + + for (size_t visualIndex = 0; visualIndex < infos.size(); ++visualIndex) + { + const auto glyphId = infos[visualIndex].codepoint; + const auto xAdvanceBase = HbScale::hbToJuce (positions[visualIndex].x_advance); + const auto yAdvanceBase = -HbScale::hbToJuce (positions[visualIndex].y_advance); // For certain OS, Font and glyph ID combinations harfbuzz will not find extents data and // hb_font_get_glyph_extents will return false. In such cases Typeface::getGlyphBounds @@ -341,11 +355,11 @@ static std::vector lowLevelShape (const String& string, const auto whitespace = extentsDataAvailable && font.getTypefacePtr()->getGlyphBounds (font.getMetricsKind(), (int) glyphId).isEmpty() - && xAdvance > 0; + && xAdvanceBase > 0; - const auto newline = std::invoke ([&controlChars, &shapingInfos = infos, j] + const auto newline = std::invoke ([&controlChars, &shapingInfos = infos, visualIndex] { - const auto it = controlChars.find ((size_t) shapingInfos[j].cluster); + const auto it = controlChars.find ((size_t) shapingInfos[visualIndex].cluster); if (it == controlChars.end()) return false; @@ -353,23 +367,56 @@ static std::vector lowLevelShape (const String& string, return it->second == ControlCharacter::cr || it->second == ControlCharacter::lf; }); + const auto cluster = (int64) infos[visualIndex].cluster + range.getStart(); + + const auto numLigaturePlaceholders = std::max ((int64) 0, + std::abs (getNextCluster (visualIndex) - cluster) - 1); + // Tracking is only applied at the beginning of a new cluster to avoid inserting it before // diacritic marks. - const auto appliedTracking = std::exchange (lastCluster, infos[j].cluster) != infos[j].cluster + const auto appliedTracking = std::exchange (lastCluster, cluster) != cluster ? trackingAmount : 0; + const auto advanceMultiplier = numLigaturePlaceholders == 0 ? 1.0f + : 1.0f / (float) (numLigaturePlaceholders + 1); + + Point advance { xAdvanceBase * advanceMultiplier + appliedTracking, yAdvanceBase * advanceMultiplier }; + + const auto ligatureClusterNumber = cluster + (ltr ? 0 : numLigaturePlaceholders); + glyphs.push_back ({ glyphId, - (int64) infos[j].cluster + range.getStart(), - (infos[j].mask & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) != 0, + ligatureClusterNumber, + (infos[visualIndex].mask & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) != 0, whitespace, newline, - Point { HbScale::hbToJuce (xAdvance) + appliedTracking, -HbScale::hbToJuce (positions[j].y_advance) }, - Point { HbScale::hbToJuce (positions[j].x_offset), -HbScale::hbToJuce (positions[j].y_offset) }, + numLigaturePlaceholders == 0 ? (int8_t) 0 : (int8_t) -numLigaturePlaceholders , + advance, + Point { HbScale::hbToJuce (positions[visualIndex].x_offset), + -HbScale::hbToJuce (positions[visualIndex].y_offset) }, }); + + for (int l = 0; l < numLigaturePlaceholders; ++l) + { + const auto clusterDiff = l + 1; + + glyphs.push_back ({ + glyphId, + ligatureClusterNumber + (ltr ? clusterDiff : -clusterDiff), + true, + whitespace, + newline, + (int8_t) (l + 1), + advance, + Point{}, + }); + } } + if (! ltr) + std::reverse (glyphs.begin(), glyphs.end()); + return glyphs; } @@ -1229,9 +1276,9 @@ void SimpleShapedText::shape (const String& data, .fillLines (shaper); auto& lineData = lineDataAndStorage.lines; - foldLinesBeyondLineLimit (lineData, (size_t) options.getMaxNumLines() - lineNumbers.size()); + foldLinesBeyondLineLimit (lineData, (size_t) options.getMaxNumLines() - lineNumbersForGlyphRanges.size()); - if (lineNumbers.size() >= (size_t) options.getMaxNumLines()) + if (lineNumbersForGlyphRanges.size() >= (size_t) options.getMaxNumLines()) break; for (const auto& line : lineData) @@ -1269,8 +1316,23 @@ void SimpleShapedText::shape (const String& data, ops.clear(); } + const auto lineTextRange = std::accumulate (glyphSpansInLine.begin(), + glyphSpansInLine.end(), + std::make_pair (std::numeric_limits::max(), + std::numeric_limits::min()), + [&] (auto& sum, auto& elem) -> std::pair + { + const auto r = elem.textRange + lineRange.getStart(); + + return { std::min (sum.first, r.getStart()), + std::max (sum.second, r.getEnd()) }; + }); + + lineTextRanges.set ({ lineTextRange.first, lineTextRange.second }, ops); + ops.clear(); + const auto lineEnd = (int64) glyphsInVisualOrder.size(); - lineNumbers.set ({ lineStart, lineEnd}, (int64) lineNumbers.size(), ops); + lineNumbersForGlyphRanges.set ({ lineStart, lineEnd}, (int64) lineNumbersForGlyphRanges.size(), ops); ops.clear(); } } @@ -1339,6 +1401,13 @@ Range SimpleShapedText::getTextRange (int64 glyphIndex) const return Range::withStartAndLength (cluster, std::max ((int64) 1, nextAdjacentCluster - cluster)); } +bool SimpleShapedText::isLtr (int64 glyphIndex) const +{ + const auto it = glyphLookup.find (glyphsInVisualOrder[(size_t) glyphIndex].cluster); + jassert (it != glyphLookup.end()); + return it->value.ltr; +} + /* Returns the first element that equals value, if such an element exists. Otherwise, returns the last element that is smaller than value, if such an element exists. @@ -1351,8 +1420,11 @@ Range SimpleShapedText::getTextRange (int64 glyphIndex) const lessThanOrEqual: less than or equal */ template -auto lessThanOrEqual (It begin, It end, Value v, Callback extractValue) +auto equalOrLessThan (It begin, It end, Value v, Callback extractValue) { + if (begin == end) + return end; + auto it = std::lower_bound (begin, end, v, @@ -1361,7 +1433,7 @@ auto lessThanOrEqual (It begin, It end, Value v, Callback extractValue) return extractValue (elem) < value; }); - if (it == end || it == begin || extractValue (*it) == v) + if (it == begin || (it != end && extractValue (*it) == v)) return it; --it; @@ -1384,7 +1456,7 @@ void SimpleShapedText::getGlyphRanges (Range textRange, std::vector auto& { return elem.cluster; }); @@ -1417,6 +1489,27 @@ void SimpleShapedText::getGlyphRanges (Range textRange, std::vectorvalue.ltr) + { + for (auto i = glyphIndex + 1; i < it->value.glyphRange.getEnd(); ++i) + if (const auto nextCluster = glyphsInVisualOrder[(size_t) i].cluster; nextCluster != cluster) + return nextCluster; + } + else + { + for (auto i = glyphIndex - 1; i >= it->value.glyphRange.getStart(); --i) + if (const auto nextCluster = glyphsInVisualOrder[(size_t) i].cluster; nextCluster != cluster) + return nextCluster; + } + + return it->range.getEnd(); +} + #if JUCE_UNIT_TESTS struct SimpleShapedTextTests : public UnitTest diff --git a/modules/juce_graphics/detail/juce_SimpleShapedText.h b/modules/juce_graphics/detail/juce_SimpleShapedText.h index 6bdcef5731..d3cd1635c4 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.h +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.h @@ -201,6 +201,7 @@ struct ShapedGlyph bool unsafeToBreakIn, bool whitespaceIn, bool newlineIn, + int8_t distanceFromLigatureIn, Point advanceIn, Point offsetIn) : advance (advanceIn), @@ -209,12 +210,20 @@ struct ShapedGlyph glyphId (glyphIdIn), unsafeToBreak (unsafeToBreakIn), whitespace (whitespaceIn), - newline (newlineIn) {} + newline (newlineIn), + distanceFromLigature (distanceFromLigatureIn) {} bool isUnsafeToBreak() const { return unsafeToBreak; } bool isWhitespace() const { return whitespace; } bool isNewline() const { return newline; } + bool isNonLigature() const { return distanceFromLigature == 0; } + bool isLigature() const { return distanceFromLigature < 0; } + bool isPlaceholderForLigature() const { return distanceFromLigature > 0; } + + int8_t getDistanceFromLigature() const { return distanceFromLigature; } + int8_t getNumTrailingLigaturePlaceholders() const { return -distanceFromLigature; } + Point advance; Point offset; int64 cluster; @@ -225,6 +234,7 @@ private: int8_t unsafeToBreak; int8_t whitespace; int8_t newline; + int8_t distanceFromLigature; }; struct GlyphLookupEntry @@ -242,18 +252,30 @@ public: SimpleShapedText (const String* data, const ShapedTextOptions& options); - /* The returned container associates line numbers with the range of glyphs (not input codepoints) - that make up the line. - */ - const auto& getLineNumbers() const { return lineNumbers; } + const auto& getLineNumbersForGlyphRanges() const { return lineNumbersForGlyphRanges; } + + const auto& getLineTextRanges() const { return lineTextRanges; } const auto& getResolvedFonts() const { return resolvedFonts; } Range getTextRange (int64 glyphIndex) const; + /* Returns true if the specified glyph is inside to an LTR run. + */ + bool isLtr (int64 glyphIndex) const; + void getGlyphRanges (Range textRange, std::vector>& outRanges) const; - int64 getNumLines() const { return (int64) lineNumbers.getRanges().size(); } + /* Returns the input codepoint index that follows the glyph in a logical sense. So for LTR text + this is the cluster number of the glyph to the right. For RTL text it's the one on the left. + + If there is no subsequent glyph, the returned number is the first Unicode codepoint index + that isn't covered by the cluster to which the selected glyph belongs, so for the glyph 'o' + in "hello" this would be 5, given there are no ligatures in use. + */ + int64 getTextIndexAfterGlyph (int64 glyphIndex) const; + + int64 getNumLines() const { return (int64) lineNumbersForGlyphRanges.getRanges().size(); } int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); } juce_wchar getCodepoint (int64 glyphIndex) const; @@ -262,13 +284,16 @@ public: Span getGlyphs() const; + const auto& getGlyphLookup() const { return glyphLookup; } + private: void shape (const String& data, const ShapedTextOptions& options); const String& string; std::vector glyphsInVisualOrder; - detail::RangedValues lineNumbers; + detail::RangedValues lineNumbersForGlyphRanges; + detail::Ranges lineTextRanges; detail::RangedValues resolvedFonts; detail::RangedValues glyphLookup; diff --git a/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp b/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp index c51ec87c01..60f514d789 100644 --- a/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp +++ b/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp @@ -182,6 +182,9 @@ static void addGlyphsFromShapedText (GlyphArrangement& ga, const detail::ShapedT auto& glyph = shapedGlyphs[i]; auto& position = positions[i]; + if (glyph.isPlaceholderForLigature()) + continue; + PositionedGlyph pg { font, st.getText()[(int) st.getTextRange (glyphIndex).getStart()], (int) glyph.glyphId, diff --git a/modules/juce_graphics/fonts/juce_TextLayout.cpp b/modules/juce_graphics/fonts/juce_TextLayout.cpp index 083ef97c42..c38751420e 100644 --- a/modules/juce_graphics/fonts/juce_TextLayout.cpp +++ b/modules/juce_graphics/fonts/juce_TextLayout.cpp @@ -346,7 +346,7 @@ static Range getLineInputRange (const detail::ShapedText& st, int64 lineN using namespace detail; return getInputRange (st, st.getSimpleShapedText() - .getLineNumbers() + .getLineNumbersForGlyphRanges() .getItem ((size_t) lineNumber).range); } @@ -360,7 +360,7 @@ static MaxFontAscentAndDescent getMaxFontAscentAndDescentInEnclosingLine (const { const auto sst = st.getSimpleShapedText(); - const auto lineRange = sst.getLineNumbers() + const auto lineRange = sst.getLineNumbersForGlyphRanges() .getItemWithEnclosingRange (lineChunkRange.getStart())->range; const auto fonts = sst.getResolvedFonts().getIntersectionsWith (lineRange); @@ -427,7 +427,7 @@ void TextLayout::createStandardLayout (const AttributedString& text) std::unique_ptr line; st.accessTogetherWith ([&] (Span glyphs, - Span> positions, + Span> positions, Font font, Range glyphRange, LineMetrics lineMetrics, @@ -470,7 +470,14 @@ void TextLayout::createStandardLayout (const AttributedString& text) }(); for (size_t i = 0; i < beyondLastNonWhitespace; ++i) - run->glyphs.add ({ (int) glyphs[i].glyphId, positions[i] - line->lineOrigin, glyphs[i].advance.x }); + { + if (glyphs[i].isPlaceholderForLigature()) + continue; + + run->glyphs.add ({ (int) glyphs[i].glyphId, + positions[i] - line->lineOrigin, + glyphs[i].advance.x }); + } line->runs.add (std::move (run)); },