1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

ShapedText: Break ligatures into multiple iterable placeholder glyphs

This commit is contained in:
attila 2025-03-05 18:09:25 +01:00 committed by Attila Szarvas
parent bc093fa64c
commit 427852836c
8 changed files with 282 additions and 70 deletions

View file

@ -221,7 +221,7 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
std::vector<LineInfo> 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<float> p) const
int64 JustifiedText::getGlyphIndexToTheRightOf (Point<float> 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<float> 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<float> 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<float> JustifiedText::getGlyphsBounds (Range<int64> glyphRange) co
{
RectangleList<float> bounds;
if (linesMetrics.isEmpty())
if (lineMetricsForGlyphRange.isEmpty())
return bounds;
const auto getBounds = [&] (const LineMetrics& line, int64 lineStart, int64 boundsStart, int64 boundsEnd) -> Rectangle<float>
@ -569,7 +572,7 @@ RectangleList<float> JustifiedText::getGlyphsBounds (Range<int64> 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<float> JustifiedText::getGlyphsBounds (Range<int64> 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)

View file

@ -113,10 +113,11 @@ public:
void accessTogetherWith (Callable&& callback, RangedValues&&... rangedValues) const
{
std::optional<int64> lastLine;
int64 lastGlyph = 0;
Point<float> 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<int8_t> 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<const ShapedGlyph>
{
if (dt == DrawType::ellipsis)
@ -170,7 +199,7 @@ public:
*/
auto& getMinimumRequiredWidthForLines() const { return minimumRequiredWidthsForLine; }
int64 getGlyphIndexAt (Point<float> p) const;
int64 getGlyphIndexToTheRightOf (Point<float> 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<LineMetrics> linesMetrics;
detail::RangedValues<LineMetrics> lineMetricsForGlyphRange;
std::optional<SimpleShapedText> ellipsis;
detail::RangedValues<DrawType> rangesToDraw;
detail::RangedValues<float> whitespaceStretch;

View file

@ -59,9 +59,14 @@ public:
return simpleShapedText.getNumGlyphs();
}
const detail::RangedValues<LineMetrics>& getLinesMetrics() const
const detail::RangedValues<LineMetrics>& 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<float> p) const
auto isLtr (int64 glyphIndex) const
{
return justifiedText.getGlyphIndexAt (p);
return simpleShapedText.isLtr (glyphIndex);
}
int64 getTextIndexForCaret (Point<float> 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<int64> textRange, std::vector<Range<int64>>& outRanges) const
@ -139,9 +175,14 @@ int64 ShapedText::getNumGlyphs() const
return impl->getNumGlyphs();
}
const detail::RangedValues<LineMetrics>& ShapedText::getLinesMetrics() const
const detail::RangedValues<LineMetrics>& 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<int64> ShapedText::getTextRange (int64 glyphIndex) const
return impl->getTextRange (glyphIndex);
}
int64 ShapedText::getGlyphIndexAt (Point<float> p) const
bool ShapedText::isLtr (int64 glyphIndex) const
{
return impl->getGlyphIndexAt (p);
return impl->isLtr (glyphIndex);
}
int64 ShapedText::getTextIndexForCaret (Point<float> p) const
{
return impl->getTextIndexForCaret (p);
}
void ShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const

View file

@ -52,6 +52,8 @@ public:
/* Returns the text which was used to construct this object. */
const String& getText() const;
Span<const ShapedGlyph> 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<int64> getTextRange (int64 glyphIndex) const;
int64 getGlyphIndexAt (Point<float> p) const;
bool isLtr (int64 glyphIndex) const;
int64 getTextIndexForCaret (Point<float> p) const;
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const;
@ -94,7 +98,9 @@ public:
int64 getNumGlyphs() const;
const detail::RangedValues<LineMetrics>& getLinesMetrics() const;
const detail::RangedValues<LineMetrics>& getLineMetricsForGlyphRange() const;
const detail::Ranges& getLineTextRanges() const;
/* @internal */
const JustifiedText& getJustifiedText() const;

View file

@ -317,14 +317,28 @@ static std::vector<ShapedGlyph> lowLevelShape (const String& string,
std::vector<size_t> characterLookup;
std::vector<ShapedGlyph> glyphs;
std::optional<uint32_t> lastCluster;
std::optional<int64> lastCluster;
for (size_t i = 0; i < infos.size(); ++i)
const auto ltr = (embeddingLevel % 2) == 0;
const auto getNextCluster = [&ltr, &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<ShapedGlyph> 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<ShapedGlyph> 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<float> 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<float> { HbScale::hbToJuce (xAdvance) + appliedTracking, -HbScale::hbToJuce (positions[j].y_advance) },
Point<float> { HbScale::hbToJuce (positions[j].x_offset), -HbScale::hbToJuce (positions[j].y_offset) },
numLigaturePlaceholders == 0 ? (int8_t) 0 : (int8_t) -numLigaturePlaceholders ,
advance,
Point<float> { 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<float>{},
});
}
}
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<int64>::max(),
std::numeric_limits<int64>::min()),
[&] (auto& sum, auto& elem) -> std::pair<int64, int64>
{
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<int64> SimpleShapedText::getTextRange (int64 glyphIndex) const
return Range<int64>::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<int64> SimpleShapedText::getTextRange (int64 glyphIndex) const
lessThanOrEqual: less than or equal
*/
template <typename It, typename Value, typename Callback>
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<int64> textRange, std::vector<Range
const auto getGlyphSubRange = [&] (auto begin, auto end)
{
auto startIt = lessThanOrEqual (begin,
auto startIt = equalOrLessThan (begin,
end,
textSubRange.getStart(),
[] (auto& elem) -> auto& { return elem.cluster; });
@ -1417,6 +1489,27 @@ void SimpleShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range
outRanges = std::move (glyphRanges.getRanges());
}
int64 SimpleShapedText::getTextIndexAfterGlyph (int64 glyphIndex) const
{
const auto& cluster = glyphsInVisualOrder[(size_t) glyphIndex].cluster;
auto it = glyphLookup.find (glyphsInVisualOrder[(size_t) glyphIndex].cluster);
if (it->value.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

View file

@ -201,6 +201,7 @@ struct ShapedGlyph
bool unsafeToBreakIn,
bool whitespaceIn,
bool newlineIn,
int8_t distanceFromLigatureIn,
Point<float> advanceIn,
Point<float> 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<float> advance;
Point<float> 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<int64> getTextRange (int64 glyphIndex) const;
/* Returns true if the specified glyph is inside to an LTR run.
*/
bool isLtr (int64 glyphIndex) const;
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& 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<const ShapedGlyph> getGlyphs() const;
const auto& getGlyphLookup() const { return glyphLookup; }
private:
void shape (const String& data,
const ShapedTextOptions& options);
const String& string;
std::vector<ShapedGlyph> glyphsInVisualOrder;
detail::RangedValues<int64> lineNumbers;
detail::RangedValues<int64> lineNumbersForGlyphRanges;
detail::Ranges lineTextRanges;
detail::RangedValues<Font> resolvedFonts;
detail::RangedValues<GlyphLookupEntry> glyphLookup;

View file

@ -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,

View file

@ -346,7 +346,7 @@ static Range<int64> 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> line;
st.accessTogetherWith ([&] (Span<const ShapedGlyph> glyphs,
Span<Point<float>> positions,
Span<const Point<float>> positions,
Font font,
Range<int64> 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));
},