mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
ShapedText: Break ligatures into multiple iterable placeholder glyphs
This commit is contained in:
parent
bc093fa64c
commit
427852836c
8 changed files with 282 additions and 70 deletions
|
|
@ -221,7 +221,7 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
|
||||||
|
|
||||||
std::vector<LineInfo> lineInfos;
|
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
|
// This is guaranteed by the RangedValues implementation. You can't assign a value to an
|
||||||
// empty range.
|
// empty range.
|
||||||
|
|
@ -259,7 +259,7 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
|
||||||
const auto containsHardBreak = shapedText.getCodepoint (range.getEnd() - 1) == 0xa
|
const auto containsHardBreak = shapedText.getCodepoint (range.getEnd() - 1) == 0xa
|
||||||
|| shapedText.getCodepoint (range.getStart()) == 0xa;
|
|| shapedText.getCodepoint (range.getStart()) == 0xa;
|
||||||
|
|
||||||
if (containsHardBreak || lineNumber == shapedText.getLineNumbers().back().value)
|
if (containsHardBreak || lineNumber == shapedText.getLineNumbersForGlyphRanges().back().value)
|
||||||
{
|
{
|
||||||
m.extraWhitespaceAdvance = {};
|
m.extraWhitespaceAdvance = {};
|
||||||
m.stretchableWhitespaces = {};
|
m.stretchableWhitespaces = {};
|
||||||
|
|
@ -282,7 +282,7 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
|
||||||
|
|
||||||
for (const auto [lineIndex, lineInfo] : enumerate (lineInfos))
|
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 range = lineNumber.range;
|
||||||
|
|
||||||
const auto maxDescent = lineInfo.lineHeight - lineInfo.maxAscent;
|
const auto maxDescent = lineInfo.lineHeight - lineInfo.maxAscent;
|
||||||
|
|
@ -291,16 +291,17 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
|
||||||
if (! top.has_value())
|
if (! top.has_value())
|
||||||
top = baseline - (1.0f + leading) * lineInfo.maxAscent;
|
top = baseline - (1.0f + leading) * lineInfo.maxAscent;
|
||||||
|
|
||||||
linesMetrics.set (range,
|
lineMetricsForGlyphRange.set (range,
|
||||||
{ lineNumber.value,
|
{ lineNumber.value,
|
||||||
{ lineInfo.mainAxisLineAlignment.anchor, baseline },
|
{ lineInfo.mainAxisLineAlignment.anchor, baseline },
|
||||||
lineInfo.maxAscent,
|
lineInfo.maxAscent,
|
||||||
lineInfo.lineHeight - lineInfo.maxAscent,
|
lineInfo.lineHeight - lineInfo.maxAscent,
|
||||||
lineInfo.mainAxisLineAlignment.effectiveLineLength + lineInfo.mainAxisLineAlignment.extraWhitespaceAdvance,
|
lineInfo.mainAxisLineAlignment.effectiveLineLength
|
||||||
*top,
|
+ lineInfo.mainAxisLineAlignment.extraWhitespaceAdvance,
|
||||||
nextLineTop },
|
*top,
|
||||||
ops,
|
nextLineTop },
|
||||||
MergeEqualItemsNo{});
|
ops,
|
||||||
|
MergeEqualItemsNo{});
|
||||||
|
|
||||||
whitespaceStretch.set (range, 0.0f, ops);
|
whitespaceStretch.set (range, 0.0f, ops);
|
||||||
const auto stretchRange = lineInfo.mainAxisLineAlignment.stretchableWhitespaces + range.getStart();
|
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
|
// The remaining logic below is for supporting
|
||||||
// GlyphArrangement::addFittedText() when the maximum number of lines is
|
// GlyphArrangement::addFittedText() when the maximum number of lines is
|
||||||
// constrained.
|
// constrained.
|
||||||
if (linesMetrics.isEmpty())
|
if (lineMetricsForGlyphRange.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto lastLineMetrics = linesMetrics.back();
|
const auto lastLineMetrics = lineMetricsForGlyphRange.back();
|
||||||
const auto lastLineGlyphRange = lastLineMetrics.range;
|
const auto lastLineGlyphRange = lastLineMetrics.range;
|
||||||
const auto lastLineGlyphs = shapedText.getGlyphs (lastLineGlyphRange);
|
const auto lastLineGlyphs = shapedText.getGlyphs (lastLineGlyphRange);
|
||||||
const auto lastLineLengths = getMainAxisLineLength (lastLineGlyphs);
|
const auto lastLineLengths = getMainAxisLineLength (lastLineGlyphs);
|
||||||
|
|
@ -469,12 +470,12 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
|
||||||
realign.extraWhitespaceAdvance, ops);
|
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;
|
float lineTop = 0.0f;
|
||||||
|
|
||||||
while (lineIt != linesMetrics.end())
|
while (lineIt != lineMetricsForGlyphRange.end())
|
||||||
{
|
{
|
||||||
const auto nextLineTop = lineIt->value.nextLineTop;
|
const auto nextLineTop = lineIt->value.nextLineTop;
|
||||||
|
|
||||||
|
|
@ -485,7 +486,7 @@ int64 JustifiedText::getGlyphIndexAt (Point<float> p) const
|
||||||
++lineIt;
|
++lineIt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lineIt == linesMetrics.end())
|
if (lineIt == lineMetricsForGlyphRange.end())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
const auto glyphsInLine = shapedText.getGlyphs (lineIt->range);
|
const auto glyphsInLine = shapedText.getGlyphs (lineIt->range);
|
||||||
|
|
@ -495,7 +496,7 @@ int64 JustifiedText::getGlyphIndexAt (Point<float> p) const
|
||||||
|
|
||||||
for (const auto& glyph : glyphsInLine)
|
for (const auto& glyph : glyphsInLine)
|
||||||
{
|
{
|
||||||
if ( p.getX() <= glyphX
|
if ( p.getX() < glyphX + glyph.advance.getX() / 2.0f
|
||||||
|| glyph.isNewline()
|
|| glyph.isNewline()
|
||||||
|| (glyphIndex - lineIt->range.getStart() == (int64) glyphsInLine.size() - 1 && glyph.isWhitespace()))
|
|| (glyphIndex - lineIt->range.getStart() == (int64) glyphsInLine.size() - 1 && glyph.isWhitespace()))
|
||||||
{
|
{
|
||||||
|
|
@ -513,10 +514,12 @@ GlyphAnchorResult JustifiedText::getGlyphAnchor (int64 index) const
|
||||||
{
|
{
|
||||||
jassert (index >= 0);
|
jassert (index >= 0);
|
||||||
|
|
||||||
if (linesMetrics.isEmpty())
|
if (lineMetricsForGlyphRange.isEmpty())
|
||||||
return {};
|
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();
|
const auto indexInLine = index - lineItem.range.getStart();
|
||||||
|
|
||||||
GlyphAnchorResult anchor { lineItem.value.anchor, lineItem.value.maxAscent, lineItem.value.maxDescent };
|
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;
|
RectangleList<float> bounds;
|
||||||
|
|
||||||
if (linesMetrics.isEmpty())
|
if (lineMetricsForGlyphRange.isEmpty())
|
||||||
return bounds;
|
return bounds;
|
||||||
|
|
||||||
const auto getBounds = [&] (const LineMetrics& line, int64 lineStart, int64 boundsStart, int64 boundsEnd) -> Rectangle<float>
|
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();)
|
for (auto consumeFrom = glyphRange.getStart(); consumeFrom < glyphRange.getEnd();)
|
||||||
{
|
{
|
||||||
const auto lineItem = linesMetrics.getItemWithEnclosingRange (consumeFrom);
|
const auto lineItem = lineMetricsForGlyphRange.getItemWithEnclosingRange (consumeFrom);
|
||||||
|
|
||||||
if (! lineItem.has_value())
|
if (! lineItem.has_value())
|
||||||
break;
|
break;
|
||||||
|
|
@ -585,10 +588,10 @@ RectangleList<float> JustifiedText::getGlyphsBounds (Range<int64> glyphRange) co
|
||||||
|
|
||||||
float JustifiedText::getHeight() const
|
float JustifiedText::getHeight() const
|
||||||
{
|
{
|
||||||
if (linesMetrics.isEmpty())
|
if (lineMetricsForGlyphRange.isEmpty())
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
|
|
||||||
return linesMetrics.back().value.nextLineTop;
|
return lineMetricsForGlyphRange.back().value.nextLineTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawJustifiedText (const JustifiedText& text, const Graphics& g, AffineTransform transform)
|
void drawJustifiedText (const JustifiedText& text, const Graphics& g, AffineTransform transform)
|
||||||
|
|
|
||||||
|
|
@ -113,10 +113,11 @@ public:
|
||||||
void accessTogetherWith (Callable&& callback, RangedValues&&... rangedValues) const
|
void accessTogetherWith (Callable&& callback, RangedValues&&... rangedValues) const
|
||||||
{
|
{
|
||||||
std::optional<int64> lastLine;
|
std::optional<int64> lastLine;
|
||||||
|
int64 lastGlyph = 0;
|
||||||
Point<float> anchor {};
|
Point<float> anchor {};
|
||||||
|
|
||||||
for (const auto item : makeIntersectingRangedValues (&shapedText.getResolvedFonts(),
|
for (const auto item : makeIntersectingRangedValues (&shapedText.getResolvedFonts(),
|
||||||
&linesMetrics,
|
&lineMetricsForGlyphRange,
|
||||||
&rangesToDraw,
|
&rangesToDraw,
|
||||||
&whitespaceStretch,
|
&whitespaceStretch,
|
||||||
(&rangedValues)...))
|
(&rangedValues)...))
|
||||||
|
|
@ -127,6 +128,34 @@ public:
|
||||||
if (std::exchange (lastLine, lineMetrics.lineNumber) != lineMetrics.lineNumber)
|
if (std::exchange (lastLine, lineMetrics.lineNumber) != lineMetrics.lineNumber)
|
||||||
anchor = lineMetrics.anchor;
|
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>
|
const auto glyphs = [this, r = range, dt = drawType]() -> Span<const ShapedGlyph>
|
||||||
{
|
{
|
||||||
if (dt == DrawType::ellipsis)
|
if (dt == DrawType::ellipsis)
|
||||||
|
|
@ -170,7 +199,7 @@ public:
|
||||||
*/
|
*/
|
||||||
auto& getMinimumRequiredWidthForLines() const { return minimumRequiredWidthsForLine; }
|
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,
|
/* 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
|
then the returned anchor specifies the location where the next glyph would have to be
|
||||||
|
|
@ -190,11 +219,11 @@ public:
|
||||||
*/
|
*/
|
||||||
float getHeight() const;
|
float getHeight() const;
|
||||||
|
|
||||||
const auto& getLinesMetrics() const { return linesMetrics; }
|
const auto& getLineMetricsForGlyphRange() const { return lineMetricsForGlyphRange; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const SimpleShapedText& shapedText;
|
const SimpleShapedText& shapedText;
|
||||||
detail::RangedValues<LineMetrics> linesMetrics;
|
detail::RangedValues<LineMetrics> lineMetricsForGlyphRange;
|
||||||
std::optional<SimpleShapedText> ellipsis;
|
std::optional<SimpleShapedText> ellipsis;
|
||||||
detail::RangedValues<DrawType> rangesToDraw;
|
detail::RangedValues<DrawType> rangesToDraw;
|
||||||
detail::RangedValues<float> whitespaceStretch;
|
detail::RangedValues<float> whitespaceStretch;
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,14 @@ public:
|
||||||
return simpleShapedText.getNumGlyphs();
|
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
|
auto& getText() const
|
||||||
|
|
@ -74,9 +79,40 @@ public:
|
||||||
return simpleShapedText.getTextRange (glyphIndex);
|
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
|
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const
|
||||||
|
|
@ -139,9 +175,14 @@ int64 ShapedText::getNumGlyphs() const
|
||||||
return impl->getNumGlyphs();
|
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
|
const String& ShapedText::getText() const
|
||||||
|
|
@ -154,9 +195,14 @@ Range<int64> ShapedText::getTextRange (int64 glyphIndex) const
|
||||||
return impl->getTextRange (glyphIndex);
|
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
|
void ShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ public:
|
||||||
/* Returns the text which was used to construct this object. */
|
/* Returns the text which was used to construct this object. */
|
||||||
const String& getText() const;
|
const String& getText() const;
|
||||||
|
|
||||||
|
Span<const ShapedGlyph> getGlyphs() const;
|
||||||
|
|
||||||
/* Returns the text's codepoint range, to which the glyph under the provided index belongs.
|
/* 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
|
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;
|
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;
|
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const;
|
||||||
|
|
||||||
|
|
@ -94,7 +98,9 @@ public:
|
||||||
|
|
||||||
int64 getNumGlyphs() const;
|
int64 getNumGlyphs() const;
|
||||||
|
|
||||||
const detail::RangedValues<LineMetrics>& getLinesMetrics() const;
|
const detail::RangedValues<LineMetrics>& getLineMetricsForGlyphRange() const;
|
||||||
|
|
||||||
|
const detail::Ranges& getLineTextRanges() const;
|
||||||
|
|
||||||
/* @internal */
|
/* @internal */
|
||||||
const JustifiedText& getJustifiedText() const;
|
const JustifiedText& getJustifiedText() const;
|
||||||
|
|
|
||||||
|
|
@ -317,14 +317,28 @@ static std::vector<ShapedGlyph> lowLevelShape (const String& string,
|
||||||
std::vector<size_t> characterLookup;
|
std::vector<size_t> characterLookup;
|
||||||
std::vector<ShapedGlyph> glyphs;
|
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 = [<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;
|
if (next < 0)
|
||||||
const auto xAdvance = positions[j].x_advance;
|
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
|
// 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
|
// 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
|
const auto whitespace = extentsDataAvailable
|
||||||
&& font.getTypefacePtr()->getGlyphBounds (font.getMetricsKind(), (int) glyphId).isEmpty()
|
&& 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())
|
if (it == controlChars.end())
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -353,23 +367,56 @@ static std::vector<ShapedGlyph> lowLevelShape (const String& string,
|
||||||
return it->second == ControlCharacter::cr || it->second == ControlCharacter::lf;
|
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
|
// Tracking is only applied at the beginning of a new cluster to avoid inserting it before
|
||||||
// diacritic marks.
|
// diacritic marks.
|
||||||
const auto appliedTracking = std::exchange (lastCluster, infos[j].cluster) != infos[j].cluster
|
const auto appliedTracking = std::exchange (lastCluster, cluster) != cluster
|
||||||
? trackingAmount
|
? trackingAmount
|
||||||
: 0;
|
: 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 ({
|
glyphs.push_back ({
|
||||||
glyphId,
|
glyphId,
|
||||||
(int64) infos[j].cluster + range.getStart(),
|
ligatureClusterNumber,
|
||||||
(infos[j].mask & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) != 0,
|
(infos[visualIndex].mask & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) != 0,
|
||||||
whitespace,
|
whitespace,
|
||||||
newline,
|
newline,
|
||||||
Point<float> { HbScale::hbToJuce (xAdvance) + appliedTracking, -HbScale::hbToJuce (positions[j].y_advance) },
|
numLigaturePlaceholders == 0 ? (int8_t) 0 : (int8_t) -numLigaturePlaceholders ,
|
||||||
Point<float> { HbScale::hbToJuce (positions[j].x_offset), -HbScale::hbToJuce (positions[j].y_offset) },
|
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;
|
return glyphs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1229,9 +1276,9 @@ void SimpleShapedText::shape (const String& data,
|
||||||
.fillLines (shaper);
|
.fillLines (shaper);
|
||||||
auto& lineData = lineDataAndStorage.lines;
|
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;
|
break;
|
||||||
|
|
||||||
for (const auto& line : lineData)
|
for (const auto& line : lineData)
|
||||||
|
|
@ -1269,8 +1316,23 @@ void SimpleShapedText::shape (const String& data,
|
||||||
ops.clear();
|
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();
|
const auto lineEnd = (int64) glyphsInVisualOrder.size();
|
||||||
lineNumbers.set ({ lineStart, lineEnd}, (int64) lineNumbers.size(), ops);
|
lineNumbersForGlyphRanges.set ({ lineStart, lineEnd}, (int64) lineNumbersForGlyphRanges.size(), ops);
|
||||||
ops.clear();
|
ops.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1339,6 +1401,13 @@ Range<int64> SimpleShapedText::getTextRange (int64 glyphIndex) const
|
||||||
return Range<int64>::withStartAndLength (cluster, std::max ((int64) 1, nextAdjacentCluster - cluster));
|
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.
|
/* 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.
|
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
|
lessThanOrEqual: less than or equal
|
||||||
*/
|
*/
|
||||||
template <typename It, typename Value, typename Callback>
|
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,
|
auto it = std::lower_bound (begin,
|
||||||
end,
|
end,
|
||||||
v,
|
v,
|
||||||
|
|
@ -1361,7 +1433,7 @@ auto lessThanOrEqual (It begin, It end, Value v, Callback extractValue)
|
||||||
return extractValue (elem) < value;
|
return extractValue (elem) < value;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (it == end || it == begin || extractValue (*it) == v)
|
if (it == begin || (it != end && extractValue (*it) == v))
|
||||||
return it;
|
return it;
|
||||||
|
|
||||||
--it;
|
--it;
|
||||||
|
|
@ -1384,7 +1456,7 @@ void SimpleShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range
|
||||||
|
|
||||||
const auto getGlyphSubRange = [&] (auto begin, auto end)
|
const auto getGlyphSubRange = [&] (auto begin, auto end)
|
||||||
{
|
{
|
||||||
auto startIt = lessThanOrEqual (begin,
|
auto startIt = equalOrLessThan (begin,
|
||||||
end,
|
end,
|
||||||
textSubRange.getStart(),
|
textSubRange.getStart(),
|
||||||
[] (auto& elem) -> auto& { return elem.cluster; });
|
[] (auto& elem) -> auto& { return elem.cluster; });
|
||||||
|
|
@ -1417,6 +1489,27 @@ void SimpleShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range
|
||||||
outRanges = std::move (glyphRanges.getRanges());
|
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
|
#if JUCE_UNIT_TESTS
|
||||||
|
|
||||||
struct SimpleShapedTextTests : public UnitTest
|
struct SimpleShapedTextTests : public UnitTest
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ struct ShapedGlyph
|
||||||
bool unsafeToBreakIn,
|
bool unsafeToBreakIn,
|
||||||
bool whitespaceIn,
|
bool whitespaceIn,
|
||||||
bool newlineIn,
|
bool newlineIn,
|
||||||
|
int8_t distanceFromLigatureIn,
|
||||||
Point<float> advanceIn,
|
Point<float> advanceIn,
|
||||||
Point<float> offsetIn)
|
Point<float> offsetIn)
|
||||||
: advance (advanceIn),
|
: advance (advanceIn),
|
||||||
|
|
@ -209,12 +210,20 @@ struct ShapedGlyph
|
||||||
glyphId (glyphIdIn),
|
glyphId (glyphIdIn),
|
||||||
unsafeToBreak (unsafeToBreakIn),
|
unsafeToBreak (unsafeToBreakIn),
|
||||||
whitespace (whitespaceIn),
|
whitespace (whitespaceIn),
|
||||||
newline (newlineIn) {}
|
newline (newlineIn),
|
||||||
|
distanceFromLigature (distanceFromLigatureIn) {}
|
||||||
|
|
||||||
bool isUnsafeToBreak() const { return unsafeToBreak; }
|
bool isUnsafeToBreak() const { return unsafeToBreak; }
|
||||||
bool isWhitespace() const { return whitespace; }
|
bool isWhitespace() const { return whitespace; }
|
||||||
bool isNewline() const { return newline; }
|
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> advance;
|
||||||
Point<float> offset;
|
Point<float> offset;
|
||||||
int64 cluster;
|
int64 cluster;
|
||||||
|
|
@ -225,6 +234,7 @@ private:
|
||||||
int8_t unsafeToBreak;
|
int8_t unsafeToBreak;
|
||||||
int8_t whitespace;
|
int8_t whitespace;
|
||||||
int8_t newline;
|
int8_t newline;
|
||||||
|
int8_t distanceFromLigature;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GlyphLookupEntry
|
struct GlyphLookupEntry
|
||||||
|
|
@ -242,18 +252,30 @@ public:
|
||||||
SimpleShapedText (const String* data,
|
SimpleShapedText (const String* data,
|
||||||
const ShapedTextOptions& options);
|
const ShapedTextOptions& options);
|
||||||
|
|
||||||
/* The returned container associates line numbers with the range of glyphs (not input codepoints)
|
const auto& getLineNumbersForGlyphRanges() const { return lineNumbersForGlyphRanges; }
|
||||||
that make up the line.
|
|
||||||
*/
|
const auto& getLineTextRanges() const { return lineTextRanges; }
|
||||||
const auto& getLineNumbers() const { return lineNumbers; }
|
|
||||||
|
|
||||||
const auto& getResolvedFonts() const { return resolvedFonts; }
|
const auto& getResolvedFonts() const { return resolvedFonts; }
|
||||||
|
|
||||||
Range<int64> getTextRange (int64 glyphIndex) const;
|
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;
|
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(); }
|
int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); }
|
||||||
|
|
||||||
juce_wchar getCodepoint (int64 glyphIndex) const;
|
juce_wchar getCodepoint (int64 glyphIndex) const;
|
||||||
|
|
@ -262,13 +284,16 @@ public:
|
||||||
|
|
||||||
Span<const ShapedGlyph> getGlyphs() const;
|
Span<const ShapedGlyph> getGlyphs() const;
|
||||||
|
|
||||||
|
const auto& getGlyphLookup() const { return glyphLookup; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void shape (const String& data,
|
void shape (const String& data,
|
||||||
const ShapedTextOptions& options);
|
const ShapedTextOptions& options);
|
||||||
|
|
||||||
const String& string;
|
const String& string;
|
||||||
std::vector<ShapedGlyph> glyphsInVisualOrder;
|
std::vector<ShapedGlyph> glyphsInVisualOrder;
|
||||||
detail::RangedValues<int64> lineNumbers;
|
detail::RangedValues<int64> lineNumbersForGlyphRanges;
|
||||||
|
detail::Ranges lineTextRanges;
|
||||||
detail::RangedValues<Font> resolvedFonts;
|
detail::RangedValues<Font> resolvedFonts;
|
||||||
detail::RangedValues<GlyphLookupEntry> glyphLookup;
|
detail::RangedValues<GlyphLookupEntry> glyphLookup;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,9 @@ static void addGlyphsFromShapedText (GlyphArrangement& ga, const detail::ShapedT
|
||||||
auto& glyph = shapedGlyphs[i];
|
auto& glyph = shapedGlyphs[i];
|
||||||
auto& position = positions[i];
|
auto& position = positions[i];
|
||||||
|
|
||||||
|
if (glyph.isPlaceholderForLigature())
|
||||||
|
continue;
|
||||||
|
|
||||||
PositionedGlyph pg { font,
|
PositionedGlyph pg { font,
|
||||||
st.getText()[(int) st.getTextRange (glyphIndex).getStart()],
|
st.getText()[(int) st.getTextRange (glyphIndex).getStart()],
|
||||||
(int) glyph.glyphId,
|
(int) glyph.glyphId,
|
||||||
|
|
|
||||||
|
|
@ -346,7 +346,7 @@ static Range<int64> getLineInputRange (const detail::ShapedText& st, int64 lineN
|
||||||
using namespace detail;
|
using namespace detail;
|
||||||
|
|
||||||
return getInputRange (st, st.getSimpleShapedText()
|
return getInputRange (st, st.getSimpleShapedText()
|
||||||
.getLineNumbers()
|
.getLineNumbersForGlyphRanges()
|
||||||
.getItem ((size_t) lineNumber).range);
|
.getItem ((size_t) lineNumber).range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -360,7 +360,7 @@ static MaxFontAscentAndDescent getMaxFontAscentAndDescentInEnclosingLine (const
|
||||||
{
|
{
|
||||||
const auto sst = st.getSimpleShapedText();
|
const auto sst = st.getSimpleShapedText();
|
||||||
|
|
||||||
const auto lineRange = sst.getLineNumbers()
|
const auto lineRange = sst.getLineNumbersForGlyphRanges()
|
||||||
.getItemWithEnclosingRange (lineChunkRange.getStart())->range;
|
.getItemWithEnclosingRange (lineChunkRange.getStart())->range;
|
||||||
|
|
||||||
const auto fonts = sst.getResolvedFonts().getIntersectionsWith (lineRange);
|
const auto fonts = sst.getResolvedFonts().getIntersectionsWith (lineRange);
|
||||||
|
|
@ -427,7 +427,7 @@ void TextLayout::createStandardLayout (const AttributedString& text)
|
||||||
std::unique_ptr<Line> line;
|
std::unique_ptr<Line> line;
|
||||||
|
|
||||||
st.accessTogetherWith ([&] (Span<const ShapedGlyph> glyphs,
|
st.accessTogetherWith ([&] (Span<const ShapedGlyph> glyphs,
|
||||||
Span<Point<float>> positions,
|
Span<const Point<float>> positions,
|
||||||
Font font,
|
Font font,
|
||||||
Range<int64> glyphRange,
|
Range<int64> glyphRange,
|
||||||
LineMetrics lineMetrics,
|
LineMetrics lineMetrics,
|
||||||
|
|
@ -470,7 +470,14 @@ void TextLayout::createStandardLayout (const AttributedString& text)
|
||||||
}();
|
}();
|
||||||
|
|
||||||
for (size_t i = 0; i < beyondLastNonWhitespace; ++i)
|
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));
|
line->runs.add (std::move (run));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue