1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Ranges: ShapedText: Use out params for improving TextEditor performance

This commit is contained in:
attila 2025-03-05 18:58:20 +01:00 committed by Attila Szarvas
parent a4f73a37af
commit bc093fa64c
10 changed files with 494 additions and 335 deletions

View file

@ -44,7 +44,7 @@ static int64 getNumLeadingWhitespaces (Span<const ShapedGlyph> glyphs)
{
const auto it = std::find_if_not (glyphs.begin(),
glyphs.end(),
[&] (const auto& g) { return g.whitespace; });
[&] (const auto& g) { return g.isWhitespace(); });
return (int64) std::distance (glyphs.begin(), it);
}
@ -56,7 +56,7 @@ static int64 getNumTrailingWhitespaces (Span<const ShapedGlyph> glyphs)
int64 trailingWhitespaces = 0;
for (auto it = glyphs.end(); --it >= glyphs.begin() && it->whitespace;)
for (auto it = glyphs.end(); --it >= glyphs.begin() && it->isWhitespace();)
++trailingWhitespaces;
return trailingWhitespaces;
@ -71,7 +71,7 @@ static NumWhitespaces getNumWhitespaces (Span<const ShapedGlyph> glyphs)
{
const auto total = std::count_if (glyphs.begin(),
glyphs.end(),
[] (const auto& g) { return g.whitespace; });
[] (const auto& g) { return g.isWhitespace(); });
return { total, getNumLeadingWhitespaces (glyphs), getNumTrailingWhitespaces (glyphs) };
}
@ -93,7 +93,7 @@ static LineLength getMainAxisLineLength (Span<const ShapedGlyph> glyphs)
if (glyphs.empty())
return {};
for (auto it = glyphs.end(); --it >= glyphs.begin() && it->whitespace;)
for (auto it = glyphs.end(); --it >= glyphs.begin() && it->isWhitespace();)
trailingWhitespacesLength += it->advance.getX();
return { total, total - trailingWhitespacesLength };
@ -108,7 +108,7 @@ static float getMainAxisLineLength (Span<const ShapedGlyph> glyphs, bool trailin
struct MainAxisLineAlignment
{
float anchor{}, extraWhitespaceAdvance{};
float anchor{}, extraWhitespaceAdvance{}, effectiveLineLength;
Range<int64> stretchableWhitespaces;
};
@ -175,7 +175,7 @@ static MainAxisLineAlignment getMainAxisLineAlignment (Justification justificati
: 0.0f;
}();
return { mainAxisLineOffset, extraWhitespaceAdvance, stretchableWhitespaces };
return { mainAxisLineOffset, extraWhitespaceAdvance, effectiveLineLength, stretchableWhitespaces };
}
struct LineInfo
@ -276,6 +276,10 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
options.getHeight(),
leading);
detail::Ranges::Operations ops;
std::optional<float> top;
for (const auto [lineIndex, lineInfo] : enumerate (lineInfos))
{
const auto lineNumber = shapedText.getLineNumbers().getItem ((size_t) lineIndex);
@ -284,24 +288,35 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
const auto maxDescent = lineInfo.lineHeight - lineInfo.maxAscent;
const auto nextLineTop = baseline + (1.0f + leading) * maxDescent + options.getAdditiveLineSpacing();
linesMetrics.set<detail::MergeEqualItems::no> (range,
{ lineNumber.value,
{ lineInfo.mainAxisLineAlignment.anchor, baseline },
lineInfo.maxAscent,
lineInfo.lineHeight - lineInfo.maxAscent,
nextLineTop });
if (! top.has_value())
top = baseline - (1.0f + leading) * lineInfo.maxAscent;
whitespaceStretch.set (range, 0.0f);
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{});
whitespaceStretch.set (range, 0.0f, ops);
const auto stretchRange = lineInfo.mainAxisLineAlignment.stretchableWhitespaces + range.getStart();
whitespaceStretch.set (stretchRange,
lineInfo.mainAxisLineAlignment.extraWhitespaceAdvance);
lineInfo.mainAxisLineAlignment.extraWhitespaceAdvance,
ops);
ops.clear();
const auto nextLineMaxAscent = lineIndex < (int) lineInfos.size() - 1 ? lineInfos[(size_t) lineIndex + 1].maxAscent : 0.0f;
baseline = nextLineTop + (1.0f + leading) * nextLineMaxAscent;
top = nextLineTop;
}
rangesToDraw.set ({ 0, (int64) shapedText.getGlyphs().size() }, DrawType::normal);
rangesToDraw.set ({ 0, (int64) shapedText.getGlyphs().size() }, DrawType::normal, ops);
//==============================================================================
// Everything above this line should work well given none of the lines were too
@ -372,11 +387,13 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
const auto eraseLastLineFromRangesToDraw = [&]
{
rangesToDraw.eraseFrom (lastLineGlyphRange.getStart());
rangesToDraw.eraseFrom (lastLineGlyphRange.getStart(), ops);
ops.clear();
};
eraseLastLineFromRangesToDraw();
rangesToDraw.set (lastLineVisibleRangeWithoutEllipsis, DrawType::normal);
rangesToDraw.set (lastLineVisibleRangeWithoutEllipsis, DrawType::normal, ops);
ops.clear();
if (options.getEllipsis().isEmpty())
{
@ -401,12 +418,15 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
options.getTrailingWhitespacesShouldFit()));
eraseLastLineFromRangesToDraw();
rangesToDraw.set (lastLineVisibleRange, DrawType::normal);
rangesToDraw.set (lastLineVisibleRange, DrawType::normal, ops);
ops.clear();
if (cutoffAtFront)
rangesToDraw.set (Range<int64>::withStartAndLength (lastLineVisibleRange.getStart() - 1, 1), DrawType::ellipsis);
rangesToDraw.set (Range<int64>::withStartAndLength (lastLineVisibleRange.getStart() - 1, 1), DrawType::ellipsis, ops);
else
rangesToDraw.set (Range<int64>::withStartAndLength (lastLineVisibleRange.getEnd(), 1), DrawType::ellipsis);
rangesToDraw.set (Range<int64>::withStartAndLength (lastLineVisibleRange.getEnd(), 1), DrawType::ellipsis, ops);
ops.clear();
const auto lineWithEllipsisGlyphs = [&]
{
@ -444,9 +464,9 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
}();
lastLineMetrics.value.anchor.setX (realign.anchor);
whitespaceStretch.set (lastLineGlyphRange, 0.0f);
whitespaceStretch.set (lastLineGlyphRange, 0.0f, ops);
whitespaceStretch.set (realign.stretchableWhitespaces + lastLineVisibleRange.getStart(),
realign.extraWhitespaceAdvance);
realign.extraWhitespaceAdvance, ops);
}
int64 JustifiedText::getGlyphIndexAt (Point<float> p) const
@ -476,8 +496,8 @@ int64 JustifiedText::getGlyphIndexAt (Point<float> p) const
for (const auto& glyph : glyphsInLine)
{
if ( p.getX() <= glyphX
|| glyph.newline
|| (glyphIndex - lineIt->range.getStart() == (int64) glyphsInLine.size() - 1 && glyph.whitespace))
|| glyph.isNewline()
|| (glyphIndex - lineIt->range.getStart() == (int64) glyphsInLine.size() - 1 && glyph.isWhitespace()))
{
break;
}
@ -489,7 +509,7 @@ int64 JustifiedText::getGlyphIndexAt (Point<float> p) const
return glyphIndex;
}
Point<float> JustifiedText::getGlyphAnchor (int64 index) const
GlyphAnchorResult JustifiedText::getGlyphAnchor (int64 index) const
{
jassert (index >= 0);
@ -499,19 +519,70 @@ Point<float> JustifiedText::getGlyphAnchor (int64 index) const
const auto lineItem = linesMetrics.getItemWithEnclosingRange (index).value_or (linesMetrics.back());
const auto indexInLine = index - lineItem.range.getStart();
auto anchor = lineItem.value.anchor;
GlyphAnchorResult anchor { lineItem.value.anchor, lineItem.value.maxAscent, lineItem.value.maxDescent };
for (auto [i, glyph] : enumerate (shapedText.getGlyphs (lineItem.range), int64{}))
{
if (i == indexInLine)
return anchor + glyph.offset;
{
anchor.anchor += glyph.offset;
return anchor;
}
anchor += glyph.advance;
anchor.anchor += glyph.advance;
}
return anchor;
}
RectangleList<float> JustifiedText::getGlyphsBounds (Range<int64> glyphRange) const
{
RectangleList<float> bounds;
if (linesMetrics.isEmpty())
return bounds;
const auto getBounds = [&] (const LineMetrics& line, int64 lineStart, int64 boundsStart, int64 boundsEnd) -> Rectangle<float>
{
const auto glyphsBefore = shapedText.getGlyphs ({ lineStart, boundsStart });
const auto xStart = std::accumulate (glyphsBefore.begin(),
glyphsBefore.end(),
line.anchor.getX(),
[] (auto sum, auto glyph)
{
return sum + glyph.advance.getX();
});
const auto glyphs = shapedText.getGlyphs ({ boundsStart, boundsEnd });
const auto xEnd = std::accumulate (glyphs.begin(),
glyphs.end(),
xStart,
[&] (auto sum, auto glyph)
{
return sum + glyph.advance.getX();
});
return { { xStart, line.top }, { xEnd, line.nextLineTop } };
};
for (auto consumeFrom = glyphRange.getStart(); consumeFrom < glyphRange.getEnd();)
{
const auto lineItem = linesMetrics.getItemWithEnclosingRange (consumeFrom);
if (! lineItem.has_value())
break;
const auto consumeTo = std::min (glyphRange.getEnd(), lineItem->range.getEnd());
bounds.add (getBounds (lineItem->value, lineItem->range.getStart(), consumeFrom, consumeTo));
consumeFrom = consumeTo;
}
return bounds;
}
float JustifiedText::getHeight() const
{
if (linesMetrics.isEmpty())

View file

@ -55,12 +55,29 @@ struct LineMetrics
float maxAscent;
float maxDescent;
/* Effective suggests the length of trailing whitespaces will be included or not depending on
ShapedTextOptions::getTrailingWhitespacesShouldFit().
*/
float effectiveLineLength;
/* These values seem redundant given the relation between the baseline, ascent and top, but
we want to ensure exactlyEquals (top, nextLineTop) for subsequent lines.
*/
float top;
/* This will be below the current line's visual bottom if non-default leading or additive
line spacing is used.
*/
float nextLineTop;
};
struct GlyphAnchorResult
{
Point<float> anchor;
float maxAscent{};
float maxDescent{};
};
//==============================================================================
class JustifiedText
{
@ -129,7 +146,7 @@ public:
anchor += glyph.advance;
if (glyph.whitespace)
if (glyph.isWhitespace())
anchor.addXY (s, 0.0f);
return result;
@ -159,7 +176,9 @@ public:
then the returned anchor specifies the location where the next glyph would have to be
placed i.e. lastGlyphAnchor + lastGlyphAdvance.
*/
Point<float> getGlyphAnchor (int64 index) const;
GlyphAnchorResult getGlyphAnchor (int64 glyphIndex) const;
RectangleList<float> getGlyphsBounds (Range<int64> glyphRange) const;
/* Returns the vertical distance from the baseline of the first line to the bottom of the last
plus any additional line spacing that follows from the leading and additiveLineSpacing
@ -171,6 +190,8 @@ public:
*/
float getHeight() const;
const auto& getLinesMetrics() const { return linesMetrics; }
private:
const SimpleShapedText& shapedText;
detail::RangedValues<LineMetrics> linesMetrics;

View file

@ -100,14 +100,16 @@ public:
void runTest() override
{
Ranges::Operations ops;
beginTest ("Ranges::set() - basics");
{
Ranges ranges;
ranges.set ({ -3, 14 });
ranges.set ({ -3, 14 }, ops);
expectRange (ranges.get (0), { -3, 14 });
ranges.set ({ 7, 20 });
ranges.set ({ 7, 20 }, ops);
expectRange (ranges.get (0), { -3, 7 });
expectRange (ranges.get (1), { 7, 20 });
}
@ -115,9 +117,9 @@ public:
beginTest ("Ranges::set() - neighbouring ranges extents are modified");
{
Ranges ranges;
ranges.set ({ -3, 14 });
ranges.set ({ 19, 30 });
ranges.set ({ 10, 25 });
ranges.set ({ -3, 14 }, ops);
ranges.set ({ 19, 30 }, ops);
ranges.set ({ 10, 25 }, ops);
// size_t doesn't always map to an existing overload for String::operator<< on all platforms
expectEquals ((int64) ranges.size(), (int64) 3);
@ -129,12 +131,12 @@ public:
beginTest ("Ranges::set() - setting a range inside another one splits that range");
{
Ranges ranges;
ranges.set ({ -3, 14 });
ranges.set ({ -3, 14 }, ops);
expectEquals ((int64) ranges.size(), (int64) 1);
//==============================================================================
ranges.set ({ 3, 7 });
ranges.set ({ 3, 7 }, ops);
expectEquals ((int64) ranges.size(), (int64) 3);
expectRange (ranges.get (0), { -3, 3 });
@ -145,16 +147,16 @@ public:
beginTest ("Ranges::set() - old ranges falling within the bounds of a newly set are erased");
{
Ranges ranges;
ranges.set ({ 0, 5 });
ranges.set ({ 5, 10 });
ranges.set ({ 15, 20 });
ranges.set ({ 25, 30 });
ranges.set ({ 35, 50 });
ranges.set ({ 0, 5 }, ops);
ranges.set ({ 5, 10 }, ops);
ranges.set ({ 15, 20 }, ops);
ranges.set ({ 25, 30 }, ops);
ranges.set ({ 35, 50 }, ops);
expectEquals ((int64) ranges.size(), (int64) 5);
//==============================================================================
ranges.set ({ 4, 36 });
ranges.set ({ 4, 36 }, ops);
expectEquals ((int64) ranges.size(), (int64) 3);
expectRange (ranges.get (0), { 0, 4 });
@ -166,7 +168,8 @@ public:
{
Ranges ranges;
auto ops = ranges.set ({ 0, 0 });
ops.clear();
ranges.set ({ 0, 0 }, ops);
expect (ranges.isEmpty());
expect (ops.empty());
}
@ -175,9 +178,9 @@ public:
{
Ranges ranges;
ranges.set ({ 0, 48 });
ranges.set ({ 48, 127 });
ranges.set ({ 49, 94 });
ranges.set ({ 0, 48 }, ops);
ranges.set ({ 48, 127 }, ops);
ranges.set ({ 49, 94 }, ops);
expectEquals ((int64) ranges.size(), (int64) 4, "");
expectRange (ranges.get (0), { 0, 48 });
@ -190,10 +193,11 @@ public:
{
Ranges ranges;
ranges.set ({ 0, 48 });
ranges.set ({ 48, 127 });
ranges.set ({ 0, 48 }, ops);
ranges.set ({ 48, 127 }, ops);
auto ops = ranges.split (47);
ops.clear();
ranges.split (47, ops);
expectEquals ((int64) ops.size(), (int64) 1, "");
const auto op0 = std::get_if<Ranges::Ops::Split> (&ops[0]);
@ -212,10 +216,11 @@ public:
{
Ranges ranges;
ranges.set ({ 0, 48 });
ranges.set ({ 48, 127 });
ranges.set ({ 0, 48 }, ops);
ranges.set ({ 48, 127 }, ops);
auto ops = ranges.split (48);
ops.clear();
ranges.split (48, ops);
expectEquals ((int64) ops.size(), (int64) 0, "");
expectEquals ((int64) ranges.size(), (int64) 2, "");
@ -227,10 +232,10 @@ public:
{
Ranges ranges;
ranges.insert ({ -3, 14 });
ranges.insert ({ -3, 14 }, ops);
expectRange (ranges.get (0), { -3, 14 });
ranges.insert ({ 7, 20 });
ranges.insert ({ 7, 20 }, ops);
expectRange (ranges.get (0), { -3, 7 });
expectRange (ranges.get (1), { 7, 20 });
expectRange (ranges.get (2), { 20, 27 });
@ -240,8 +245,8 @@ public:
{
Ranges ranges;
ranges.insert ({ 10, 11 });
ranges.insert ({ 0, 1 });
ranges.insert ({ 10, 11 }, ops);
ranges.insert ({ 0, 1 }, ops);
expectRange (ranges.get (0), { 0, 1 });
expectRange (ranges.get (1), { 11, 12 });
@ -251,19 +256,20 @@ public:
{
Ranges ranges;
auto ops = ranges.insert ({ 0, 0 });
ops.clear();
ranges.insert ({ 0, 0 }, ops);
expect (ranges.isEmpty());
expect (ops.empty());
}
const auto getTestRanges = []
const auto getTestRanges = [&ops]
{
Ranges ranges;
ranges.set ({ 0, 48 });
ranges.set ({ 48, 49 });
ranges.set ({ 55, 94 });
ranges.set ({ 94, 127 });
ranges.set ({ 0, 48 }, ops);
ranges.set ({ 48, 49 }, ops);
ranges.set ({ 55, 94 }, ops);
ranges.set ({ 94, 127 }, ops);
return ranges;
};
@ -271,7 +277,7 @@ public:
beginTest ("Ranges::eraseFrom() - erasing beyond all ranges has no effect");
{
auto ranges = getTestRanges();
ranges.eraseFrom (ranges.get (ranges.size() - 1).getEnd() + 5);
ranges.eraseFrom (ranges.get (ranges.size() - 1).getEnd() + 5, ops);
expectEquals ((int64) ranges.size(), (int64) 4, "");
expectRange (ranges.get (0), { 0, 48 });
@ -283,7 +289,7 @@ public:
beginTest ("Ranges::eraseFrom() - erasing modifies the range that encloses the starting index");
{
auto ranges = getTestRanges();
ranges.eraseFrom (122);
ranges.eraseFrom (122, ops);
expectEquals ((int64) ranges.size(), (int64) 4, "");
expectRange (ranges.get (0), { 0, 48 });
@ -295,7 +301,7 @@ public:
beginTest ("Ranges::eraseFrom() - ranges starting after the erased index are deleted entirely");
{
auto ranges = getTestRanges();
ranges.eraseFrom (60);
ranges.eraseFrom (60, ops);
expectEquals ((int64) ranges.size(), (int64) 3, "");
expectRange (ranges.get (0), { 0, 48 });
@ -306,7 +312,7 @@ public:
beginTest ("Ranges::eraseFrom() - erasing from a location outside any ranges will still drop subsequent ranges");
{
auto ranges = getTestRanges();
ranges.eraseFrom (51);
ranges.eraseFrom (51, ops);
expectEquals ((int64) ranges.size(), (int64) 2, "");
expectRange (ranges.get (0), { 0, 48 });
@ -317,7 +323,7 @@ public:
{
auto ranges = getTestRanges();
ranges.erase ({ 30, 30 });
ranges.erase ({ 30, 30 }, ops);
expectEquals ((int64) ranges.size(), (int64) 4, "");
expectRange (ranges.get (0), { 0, 48 });
@ -330,7 +336,7 @@ public:
{
auto ranges = getTestRanges();
ranges.erase ({ 30, 31 });
ranges.erase ({ 30, 31 }, ops);
expectEquals ((int64) ranges.size(), (int64) 5, "");
expectRange (ranges.get (0), { 0, 30 });
@ -344,7 +350,7 @@ public:
{
auto ranges = getTestRanges();
ranges.erase ({ 30, 70 });
ranges.erase ({ 30, 70 }, ops);
expectEquals ((int64) ranges.size(), (int64) 3, "");
expectRange (ranges.get (0), { 0, 30 });
@ -356,7 +362,7 @@ public:
{
auto ranges = getTestRanges();
ranges.erase ({ 51, 53 });
ranges.erase ({ 51, 53 }, ops);
expectEquals ((int64) ranges.size(), (int64) 4, "");
expectRange (ranges.get (0), { 0, 48 });
@ -369,7 +375,7 @@ public:
{
auto ranges = getTestRanges();
ranges.erase ({ -1000, 1000 });
ranges.erase ({ -1000, 1000 }, ops);
expect (ranges.isEmpty());
}
@ -378,7 +384,7 @@ public:
{
auto ranges = getTestRanges();
ranges.drop ({ 48, 49 });
ranges.drop ({ 48, 49 }, ops);
expectEquals ((int64) ranges.size(), (int64) 3, "");
expectRange (ranges.get (0), { 0, 48 });
@ -390,7 +396,7 @@ public:
{
auto ranges = getTestRanges();
ranges.drop ({ 51, 53 });
ranges.drop ({ 51, 53 }, ops);
expectEquals ((int64) ranges.size(), (int64) 4, "");
expectRange (ranges.get (0), { 0, 48 });
@ -403,7 +409,7 @@ public:
{
auto ranges = getTestRanges();
ranges.drop ({ -1000, 1000 });
ranges.drop ({ -1000, 1000 }, ops);
expect (ranges.isEmpty());
}
@ -411,11 +417,11 @@ public:
{
Ranges ranges;
ranges.set ({ 0, 48 });
ranges.set ({ 48, 49 });
ranges.set ({ 55, 94 });
ranges.set ({ 94, 127 });
ranges.set ({ 127, 150 });
ranges.set ({ 0, 48 }, ops);
ranges.set ({ 48, 49 }, ops);
ranges.set ({ 55, 94 }, ops);
ranges.set ({ 94, 127 }, ops);
ranges.set ({ 127, 150 }, ops);
expect (ranges.covers ({ 0, 48 }));
expect (ranges.covers ({ 0, 20 }));
@ -460,14 +466,15 @@ public:
void runTest() override
{
auto random = getRandom();
Ranges::Operations ops;
const auto createRangedValuesObject = [&]
{
RangedValues<char> rangedValues;
rangedValues.set ({ 0, 10 }, 'a');
rangedValues.set ({ 11, 20 }, 'b');
rangedValues.set ({ 23, 30 }, 'c');
rangedValues.set ({ 0, 10 }, 'a', ops);
rangedValues.set ({ 11, 20 }, 'b', ops);
rangedValues.set ({ 23, 30 }, 'c', ops);
return rangedValues;
};
@ -476,7 +483,7 @@ public:
{
auto rangedValues = createRangedValuesObject();
rangedValues.set ({ 5, 15 }, 'd');
rangedValues.set ({ 5, 15 }, 'd', ops);
expect (! rangedValues.isEmpty());
@ -485,7 +492,7 @@ public:
expectRangedValuesItem (rangedValues.getItem (2), { 15, 20 }, 'b');
expectRangedValuesItem (rangedValues.getItem (3), { 23, 30 }, 'c');
rangedValues.set ({ 19, 24 }, 'e');
rangedValues.set ({ 19, 24 }, 'e', ops);
expectRangedValuesItem (rangedValues.getItem (2), { 15, 19 }, 'b');
expectRangedValuesItem (rangedValues.getItem (3), { 19, 24 }, 'e');
@ -496,7 +503,7 @@ public:
{
auto rangedValues = createRangedValuesObject();
rangedValues.set ({ -1, 0 }, 'd');
rangedValues.set ({ -1, 0 }, 'd', ops);
expectRangedValuesItem (rangedValues.getItem (0), { -1, 0 }, 'd');
expectRangedValuesItem (rangedValues.getItem (1), { 0, 10 }, 'a');
@ -506,7 +513,7 @@ public:
{
auto rangedValues = createRangedValuesObject();
rangedValues.set<MergeEqualItems::no> ({ 5, 15 }, 'b');
rangedValues.set ({ 5, 15 }, 'b', ops, MergeEqualItemsNo{});
expectRangedValuesItem (rangedValues.getItem (0), { 0, 5 }, 'a');
expectRangedValuesItem (rangedValues.getItem (1), { 5, 15 }, 'b');
@ -518,7 +525,7 @@ public:
{
auto rangedValues = createRangedValuesObject();
rangedValues.set<MergeEqualItems::yes> ({ 5, 15 }, 'b');
rangedValues.set ({ 5, 15 }, 'b', ops, MergeEqualItemsYes{});
expectRangedValuesItem (rangedValues.getItem (0), { 0, 5 }, 'a');
expectRangedValuesItem (rangedValues.getItem (1), { 5, 20 }, 'b');
@ -529,7 +536,8 @@ public:
{
RangedValues<char> rangedValues;
auto ops = rangedValues.set ({ 0, 0 }, 'a');
ops.clear();
rangedValues.set ({ 0, 0 }, 'a', ops);
expect (rangedValues.isEmpty());
expect (ops.empty());
}
@ -538,9 +546,9 @@ public:
{
RangedValues<char> rangedValues;
rangedValues.set ({ 0, 48 }, 'a');
rangedValues.set ({ 48, 127 }, 'b');
rangedValues.set ({ 49, 94 }, 'c');
rangedValues.set ({ 0, 48 }, 'a', ops);
rangedValues.set ({ 48, 127 }, 'b', ops);
rangedValues.set ({ 49, 94 }, 'c', ops);
expectEquals ((int64) rangedValues.size(), (int64) 4, "");
@ -581,13 +589,17 @@ public:
const auto beginInsertionAt = (int64) random.nextInt (100) - 50;
const auto numElemsToInsert = (int64) random.nextInt (1000);
rangedValuesNotMerged.insert<MergeEqualItems::no> ({ Range<int64>::withStartAndLength (beginInsertionAt, numElemsToInsert) },
'a' + (char) random.nextInt (25));
rangedValuesNotMerged.insert ({ Range<int64>::withStartAndLength (beginInsertionAt, numElemsToInsert) },
'a' + (char) random.nextInt (25),
ops,
MergeEqualItemsNo{});
expectEquals (getCumulativeRangeLengths (rangedValuesNotMerged) - totalLengthBeforeInsert, numElemsToInsert);
rangedValuesMerged.insert<MergeEqualItems::yes> ({ Range<int64>::withStartAndLength (beginInsertionAt, numElemsToInsert) },
'a' + (char) random.nextInt (25));
rangedValuesMerged.insert ({ Range<int64>::withStartAndLength (beginInsertionAt, numElemsToInsert) },
'a' + (char) random.nextInt (25),
ops,
MergeEqualItemsYes{});
expectEquals (getCumulativeRangeLengths (rangedValuesMerged) - totalLengthBeforeInsert, numElemsToInsert);
}
@ -599,7 +611,7 @@ public:
expectEquals ((int64) rangedValues.size(), (int64) 3);
rangedValues.insert ({ 2, 4 }, 'd');
rangedValues.insert ({ 2, 4 }, 'd', ops);
expectEquals ((int64) rangedValues.size(), (int64) 5);
@ -617,7 +629,7 @@ public:
expectEquals ((int64) rangedValues.size(), (int64) 3);
rangedValues.insert<MergeEqualItems::yes> ({ 2, 4 }, 'a');
rangedValues.insert ({ 2, 4 }, 'a', ops, MergeEqualItemsYes{});
expectEquals ((int64) rangedValues.size(), (int64) 3);
@ -630,7 +642,7 @@ public:
expectEquals ((int64) rangedValues.size(), (int64) 3);
rangedValues.insert<MergeEqualItems::no> ({ 2, 4 }, 'a');
rangedValues.insert ({ 2, 4 }, 'a', ops, MergeEqualItemsNo{});
expectEquals ((int64) rangedValues.size(), (int64) 5);
@ -647,16 +659,18 @@ public:
{
RangedValues<char> emptyRangedValues;
auto ops = emptyRangedValues.insert ({ 0, 0 }, 'a');
ops.clear();
emptyRangedValues.insert ({ 0, 0 }, 'a', ops);
expect (emptyRangedValues.isEmpty());
expect (ops.empty());
}
{
RangedValues<char> rangedValues;
rangedValues.set ({ 0, 10 }, 'a');
rangedValues.set ({ 0, 10 }, 'a', ops);
auto ops = rangedValues.insert ({ 0, 0 }, 'a');
ops.clear();
rangedValues.insert ({ 0, 0 }, 'a', ops);
expect (ops.empty());
expectEquals ((int64) rangedValues.size(), (int64) 1);
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
@ -667,11 +681,11 @@ public:
{
RangedValues<char> rangedValues;
rangedValues.set ({ 0, 10 }, 'a');
rangedValues.set ({ 11, 20 }, 'b');
rangedValues.set ({ 23, 30 }, 'c');
rangedValues.set ({ 35, 45 }, 'c');
rangedValues.set ({ 45, 60 }, 'd');
rangedValues.set ({ 0, 10 }, 'a', ops);
rangedValues.set ({ 11, 20 }, 'b', ops);
rangedValues.set ({ 23, 30 }, 'c', ops);
rangedValues.set ({ 35, 45 }, 'c', ops);
rangedValues.set ({ 45, 60 }, 'd', ops);
return rangedValues;
};
@ -680,7 +694,7 @@ public:
{
auto rangedValues = createRangedValuesObjectForErase();
rangedValues.erase ({ 15, 16 });
rangedValues.erase ({ 15, 16 }, ops);
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
expectRangedValuesItem (rangedValues.getItem (1), { 11, 15 }, 'b');
@ -694,7 +708,7 @@ public:
{
auto rangedValues = createRangedValuesObjectForErase();
rangedValues.eraseUpTo (rangedValues.getRanges().get (0).getStart());
rangedValues.eraseUpTo (rangedValues.getRanges().get (0).getStart(), ops);
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
expectRangedValuesItem (rangedValues.getItem (1), { 11, 20 }, 'b');
@ -707,7 +721,7 @@ public:
{
auto rangedValues = createRangedValuesObjectForErase();
rangedValues.eraseUpTo (15);
rangedValues.eraseUpTo (15, ops);
expectRangedValuesItem (rangedValues.getItem (0), { 15, 20 }, 'b');
expectRangedValuesItem (rangedValues.getItem (1), { 23, 30 }, 'c');
@ -720,7 +734,7 @@ public:
auto rangedValues = createRangedValuesObjectForErase();
const auto ranges = rangedValues.getRanges();
rangedValues.eraseUpTo (ranges.get (ranges.size() - 1).getEnd());
rangedValues.eraseUpTo (ranges.get (ranges.size() - 1).getEnd(), ops);
expect (rangedValues.isEmpty());
}
@ -729,7 +743,7 @@ public:
{
auto rangedValues = createRangedValuesObjectForErase();
rangedValues.drop<MergeEqualItems::no> ({ 15, 16 });
rangedValues.drop ({ 15, 16 }, ops, MergeEqualItemsNo{});
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
expectRangedValuesItem (rangedValues.getItem (1), { 11, 15 }, 'b');
@ -743,7 +757,7 @@ public:
{
auto rangedValues = createRangedValuesObjectForErase();
rangedValues.drop<MergeEqualItems::yes> ({ 15, 16 });
rangedValues.drop ({ 15, 16 }, ops, MergeEqualItemsYes{});
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
expectRangedValuesItem (rangedValues.getItem (1), { 11, 19 }, 'b');
@ -755,9 +769,9 @@ public:
beginTest ("RangedValues::drop() - the merge happens at the seam caused by the drop and does not extend beyond");
{
auto rangedValues = createRangedValuesObjectForErase();
rangedValues.set<MergeEqualItems::no> ({ 20, 30 }, 'b');
rangedValues.set ({ 20, 30 }, 'b', ops, MergeEqualItemsNo{});
rangedValues.drop<MergeEqualItems::yes> ({ 15, 16 });
rangedValues.drop ({ 15, 16 }, ops, MergeEqualItemsYes{});
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
@ -780,23 +794,25 @@ public:
void runTest() override
{
Ranges::Operations ops;
beginTest ("IntersectingRangedValuesTests - iterating over multiple RangedValues");
{
RangedValues<int> rv1;
rv1.set ({ 3, 8}, 1);
rv1.set ({ 9, 16}, 2);
rv1.set ({ 30, 40}, 3);
rv1.set ({ 3, 8}, 1, ops);
rv1.set ({ 9, 16}, 2, ops);
rv1.set ({ 30, 40}, 3, ops);
RangedValues<int> rv2;
rv2.set ({ 0, 4}, 7);
rv2.set ({ 4, 6}, 11);
rv2.set ({ 6, 25}, 13);
rv2.set ({ 27, 55}, 17);
rv2.set ({ 0, 4}, 7, ops);
rv2.set ({ 4, 6}, 11, ops);
rv2.set ({ 6, 25}, 13, ops);
rv2.set ({ 27, 55}, 17, ops);
RangedValues<int> rv3;
rv3.set ({ -2, 10}, -1);
rv3.set ({ 15, 19}, -2);
rv3.set ({ 22, 36}, -3);
rv3.set ({ -2, 10}, -1, ops);
rv3.set ({ 15, 19}, -2, ops);
rv3.set ({ 22, 36}, -3, ops);
int iteration = 0;

View file

@ -106,20 +106,6 @@ struct Ranges final
using Operations = std::vector<Op>;
static auto withOperationsFrom (const Operations& ops, const Operations& newOps)
{
auto result = ops;
result.insert (result.end(), newOps.begin(), newOps.end());
return result;
}
static auto withOperationsFrom (const Operations& ops, Op newOp)
{
auto result = ops;
result.insert (result.end(), newOp);
return result;
}
Ranges() = default;
explicit Ranges (std::vector<Range<int64>> rangesIn)
@ -141,42 +127,34 @@ struct Ranges final
//==============================================================================
// Basic operations
Operations split (int64 i)
void split (int64 i, Operations& ops)
{
Operations ops;
const auto elemIndex = getIndexForEnclosingRange (i);
if (! elemIndex.has_value())
return {};
return;
auto& elem = ranges[*elemIndex];
if (elem.getStart() == i)
return {};
return;
ops = withOperationsFrom (ops, Ops::Split { *elemIndex,
elem.withEnd (i),
elem.withStart (i) });
ops.push_back (Ops::Split { *elemIndex, elem.withEnd (i), elem.withStart (i) });
const auto oldLength = elem.getLength();
elem.setEnd (i);
ranges.insert (iteratorWithAdvance (ranges.begin(), *elemIndex + 1),
{ i, i + oldLength - elem.getLength() });
return ops;
}
Operations erase (Range<int64> r)
void erase (Range<int64> r, Operations& ops)
{
if (r.isEmpty())
return {};
Operations ops;
return;
for (auto i : { r.getStart(), r.getEnd() })
ops = withOperationsFrom (ops, split (i));
split (i, ops);
const auto firstToDelete = std::lower_bound (ranges.begin(),
ranges.end(),
@ -191,26 +169,23 @@ struct Ranges final
{ return elem.getStart() < value; });
if (firstToDelete != ranges.end())
ops = withOperationsFrom (ops, Ops::Erase { { getIndex (firstToDelete), getIndex (beyondLastToDelete) } });
ops.push_back (Ops::Erase { { getIndex (firstToDelete), getIndex (beyondLastToDelete) } });
ranges.erase (firstToDelete, beyondLastToDelete);
return ops;
}
Operations drop (Range<int64> r)
void drop (Range<int64> r, Operations& ops)
{
auto ops = erase (r);
ops = withOperationsFrom (ops, shift (r.getEnd(), -r.getLength()));
return ops;
erase (r, ops);
shift (r.getEnd(), -r.getLength(), ops);
}
/* Shift all ranges starting at or beyond the specified from parameter, by the specified amount.
*/
Operations shift (int64 from, int64 amount)
void shift (int64 from, int64 amount, Operations& ops)
{
if (amount == 0)
return {};
return;
const auto shiftStartingFrom = std::lower_bound (ranges.begin(),
ranges.end(),
@ -218,26 +193,20 @@ struct Ranges final
[] (auto& elem, auto& value)
{ return elem.getStart() < value; });
Operations ops;
for (auto it = shiftStartingFrom; it < ranges.end(); ++it)
{
const auto oldRange = *it;
*it += amount;
ops = withOperationsFrom (ops, Ops::Change { getIndex (it), oldRange, *it });
ops.push_back (Ops::Change { getIndex (it), oldRange, *it });
}
return ops;
}
Operations set (Range<int64> newRange)
void set (Range<int64> newRange, Operations& ops)
{
if (newRange.isEmpty())
return {};
return;
Operations ops;
ops = withOperationsFrom (ops, erase (newRange));
erase (newRange, ops);
const auto insertBefore = std::lower_bound (ranges.begin(),
ranges.end(),
@ -245,21 +214,17 @@ struct Ranges final
[] (auto& elem, auto& value)
{ return elem.getStart() < value; });
ops = withOperationsFrom (ops, Ops::New { getIndex (insertBefore) });
ops.push_back (Ops::New { getIndex (insertBefore) });
ranges.insert (insertBefore, newRange);
return ops;
}
Operations insert (Range<int64> newRange)
void insert (Range<int64> newRange, Operations& ops)
{
if (newRange.isEmpty())
return {};
return;
Operations ops;
ops = withOperationsFrom (ops, split (newRange.getStart()));
ops = withOperationsFrom (ops, shift (newRange.getStart(), newRange.getLength()));
split (newRange.getStart(), ops);
shift (newRange.getStart(), newRange.getLength(), ops);
const auto insertBefore = std::lower_bound (ranges.begin(),
ranges.end(),
@ -270,9 +235,7 @@ struct Ranges final
const auto insertBeforeIndex = getIndex (insertBefore);
ranges.insert (insertBefore, newRange);
ops = withOperationsFrom (ops, Ops::New { insertBeforeIndex });
return ops;
ops.push_back (Ops::New { insertBeforeIndex });
}
//==============================================================================
@ -282,41 +245,36 @@ struct Ranges final
ranges.clear();
}
Operations eraseFrom (int64 i)
void eraseFrom (int64 i, Operations& ops)
{
if (ranges.empty())
return {};
return;
return erase ({ i, ranges.back().getEnd() });
erase ({ i, ranges.back().getEnd() }, ops);
}
/* Merges neighbouring ranges backwards if they form a contiguous range.
*/
Operations mergeBack (size_t i)
void mergeBack (size_t i, Operations& ops)
{
jassert (isPositiveAndBelow (i, ranges.size()));
if (i == 0 || i >= ranges.size())
return {};
return;
const auto start = i - 1;
const auto end = i;
if (ranges[start].getEnd() != ranges[end].getStart())
return {};
Operations ops;
return;
const auto oldRange = ranges[start];
ranges[start].setEnd (ranges[end].getEnd());
ops = withOperationsFrom (ops, Ops::Change { start, oldRange, ranges[start] });
ops = withOperationsFrom (ops, Ops::Erase { { end, end + 1 } });
ops.push_back (Ops::Change { start, oldRange, ranges[start] });
ops.push_back (Ops::Erase { { end, end + 1 } });
ranges.erase (iteratorWithAdvance (ranges.begin(), end),
iteratorWithAdvance (ranges.begin(), end + 1));
return ops;
}
/* Returns the ranges that have an intersection with the provided range. */
@ -441,12 +399,6 @@ private:
};
//==============================================================================
enum class MergeEqualItems
{
no,
yes
};
template <typename T, typename = void>
constexpr auto hasEqualityOperator = false;
@ -469,13 +421,13 @@ private:
using InternalIterator = const Range<int64>*;
public:
using value_type = std::pair<Range<int64>, T>;
using value_type = RangedValuesIteratorItem<T>;
using difference_type = std::ptrdiff_t;
using reference = RangedValuesIteratorItem<T>;
using reference = value_type;
struct PointerProxy
{
PointerProxy (reference r) : ref { r } {}
explicit PointerProxy (reference r) : ref { r } {}
auto operator->() const { return &ref; }
@ -543,6 +495,9 @@ private:
InternalIterator iteratorBase, iterator;
};
struct MergeEqualItemsYes{};
struct MergeEqualItemsNo{};
/* Data structure for storing values associated with non-overlapping ranges.
Has set() and insert() operations with the optional merging of ranges that contain equal values.
@ -567,7 +522,7 @@ class RangedValues
return j ? std::make_optional (self.getItem (*j)) : std::nullopt;
}
auto tie() const { return std::make_tuple (ranges, values); }
auto tie() const { return std::tie (ranges, values); }
public:
static constexpr bool canMergeEqualItems = hasEqualityOperator<T>;
@ -625,70 +580,84 @@ public:
const T& value;
};
//==============================================================================
// Basic operations
template <MergeEqualItems mergeEquals = MergeEqualItems::yes>
auto set (Range<int64> r, T v)
template <typename U>
static auto createSubSpan (U& s, size_t offset)
{
static_assert (mergeEquals == MergeEqualItems::no || canMergeEqualItems,
"You can't use MergeEqualItems::yes if your type doesn't have operator==.");
Span span { s };
Ranges::Operations ops;
if (span.empty())
return span;
ops = Ranges::withOperationsFrom (ops, ranges.set (r));
applyOperations (ops, std::move (v));
const auto newSize = s.size() - std::min (s.size(), offset);
if constexpr (mergeEquals == MergeEqualItems::yes)
{
ops = Ranges::withOperationsFrom (ops, mergeEqualItems (r.getStart()));
ops = Ranges::withOperationsFrom (ops, mergeEqualItems (r.getEnd()));
}
if (newSize == 0)
return decltype (span){};
return ops;
auto start = s.begin();
std::advance (start, static_cast<typename U::difference_type> (offset));
return Span { start, newSize };
}
template <MergeEqualItems mergeEquals = MergeEqualItems::yes>
auto insert (Range<int64> r, T v)
//==============================================================================
// Basic operations
template <typename MergeEquals = MergeEqualItemsYes>
void set (Range<int64> r, T v, Ranges::Operations& ops, MergeEquals = {})
{
static_assert (mergeEquals == MergeEqualItems::no || canMergeEqualItems,
static_assert (std::is_same_v<MergeEqualItemsNo, MergeEquals> || canMergeEqualItems,
"You can't use MergeEqualItems::yes if your type doesn't have operator==.");
auto ops = ranges.insert (r);
applyOperations (ops, std::move (v));
const auto opsStart = ops.size();
ranges.set (r, ops);
applyOperations (createSubSpan (ops, opsStart), std::move (v));
if constexpr (mergeEquals == MergeEqualItems::yes)
if constexpr (std::is_same_v<MergeEquals, MergeEqualItemsYes>)
{
ops = Ranges::withOperationsFrom (ops, mergeEqualItems (r.getStart()));
ops = Ranges::withOperationsFrom (ops, mergeEqualItems (r.getEnd()));
mergeEqualItems (r.getStart(), ops);
mergeEqualItems (r.getEnd(), ops);
}
}
return ops;
template <typename MergeEquals = MergeEqualItemsYes>
void insert (Range<int64> r, T v, Ranges::Operations& ops, MergeEquals = {})
{
static_assert (std::is_same_v<MergeEquals, MergeEqualItemsNo> || canMergeEqualItems,
"You can't use MergeEqualItems::yes if your type doesn't have operator==.");
const auto opsStart = ops.size();
ranges.insert (r, ops);
applyOperations (createSubSpan (ops, opsStart), std::move (v));
if constexpr (std::is_same_v<MergeEquals, MergeEqualItemsYes>)
{
mergeEqualItems (r.getStart(), ops);
mergeEqualItems (r.getEnd(), ops);
}
}
// erase will always cause a discontinuity and thus, there is no opportunity to merge
auto erase (Range<int64> r)
void erase (Range<int64> r, Ranges::Operations& ops)
{
auto ops = ranges.erase (r);
applyOperations (ops);
return ops;
const auto opsStart = ops.size();
ranges.erase (r, ops);
applyOperations (createSubSpan (ops, opsStart));
}
// drop moves subsequent ranges downward, and can end up in these ranges forming a contiguous
// range with the ones on the left side of the drop. Hence, it makes sense to ask if we want
// merging behaviour.
template <MergeEqualItems mergeEquals = MergeEqualItems::yes>
auto drop (Range<int64> r)
template <typename MergeEquals = MergeEqualItemsYes>
void drop (Range<int64> r, Ranges::Operations& ops, MergeEquals = {})
{
static_assert (mergeEquals == MergeEqualItems::no || canMergeEqualItems,
static_assert (std::is_same_v<MergeEquals, MergeEqualItemsNo> || canMergeEqualItems,
"You can't use MergeEqualItems::yes if your type doesn't have operator==.");
auto ops = ranges.drop (r);
applyOperations (ops);
const auto opsStart = ops.size();
ranges.drop (r, ops);
applyOperations (createSubSpan (ops, opsStart));
if constexpr (mergeEquals == MergeEqualItems::yes)
ops = Ranges::withOperationsFrom (ops, mergeEqualItems (r.getStart()));
return ops;
if constexpr (std::is_same_v<MergeEquals, MergeEqualItemsYes>)
mergeEqualItems (r.getStart(), ops);
}
//==============================================================================
@ -698,40 +667,25 @@ public:
values.clear();
}
Ranges::Operations shift (int64 from, int64 amount)
void shift (int64 from, int64 amount, Ranges::Operations& ops)
{
return ranges.shift (from, amount);
ranges.shift (from, amount, ops);
}
Ranges::Operations eraseFrom (int64 i)
void eraseFrom (int64 i, Ranges::Operations& ops)
{
if (ranges.isEmpty())
return {};
return;
return erase ({ i, ranges.get (ranges.size() - 1).getEnd() });
erase ({ i, ranges.get (ranges.size() - 1).getEnd() }, ops);
}
Ranges::Operations eraseUpTo (int64 i)
void eraseUpTo (int64 i, Ranges::Operations& ops)
{
if (ranges.isEmpty())
return {};
return;
return erase ({ ranges.get (0).getStart(), i });
}
/** Create a RangedValues object from non-overlapping ranges. */
template<MergeEqualItems mergeEquals, typename Iterable>
auto setForEach (Iterable begin, Iterable end)
{
Ranges::Operations ops;
for (auto it = begin; it != end; ++it)
{
const auto& [range, value] = *it;
ops = Ranges::withOperationsFrom (ops, set<mergeEquals> (range, value));
}
return ops;
erase ({ ranges.get (0).getStart(), i }, ops);
}
auto getItemWithEnclosingRange (int64 i)
@ -802,11 +756,14 @@ public:
RangedValues<T> result;
detail::Ranges::Operations ops;
for (const auto& is : intersections)
{
auto valueIndex = ranges.getIndexForEnclosingRange (is.getStart());
jassert (valueIndex.has_value());
result.template set<MergeEqualItems::no> (is, values[*valueIndex]);
result.set (is, values[*valueIndex], ops, MergeEqualItemsNo{});
ops.clear();
}
return result;
@ -814,8 +771,9 @@ public:
RangedValues<T> getIntersectionsStartingAtZeroWith (Range<int64> r) const
{
detail::Ranges::Operations ops;
auto result = getIntersectionsWith (r);
result.drop ({ (int64) 0, r.getStart() });
result.drop ({ (int64) 0, r.getStart() }, ops);
return result;
}
@ -832,23 +790,22 @@ public:
}
private:
Ranges::Operations mergeEqualItems (int64 i)
void mergeEqualItems (int64 i, Ranges::Operations& ops)
{
const auto endOpt = ranges.getIndexForEnclosingRange (i);
if (! endOpt.has_value() || *endOpt == 0)
return {};
return;
const auto end = *endOpt;
const auto start = end - 1;
if (! exactlyEqual (values[start], values[end]))
return {};
return;
const auto ops = ranges.mergeBack (end);
applyOperations (ops);
return ops;
const auto opsStart = ops.size();
ranges.mergeBack (end, ops);
applyOperations (createSubSpan (ops, opsStart));
}
void applyOperation (const Ranges::Op& op)
@ -884,13 +841,13 @@ private:
}
}
void applyOperations (const Ranges::Operations& ops)
void applyOperations (Span<const Ranges::Op> ops)
{
for (const auto& op : ops)
applyOperation (op);
}
void applyOperations (const Ranges::Operations& ops, T v)
void applyOperations (Span<const Ranges::Op> ops, T v)
{
for (const auto& op : ops)
applyOperation (op, v);
@ -918,9 +875,6 @@ template <typename T>
class RangedIteratorWrapper final : public RangedIterator
{
public:
/* We pass a pointer rather than a reference here to make it clearer that the pointed-to object
must outlive the RangedIteratorWrapper, otherwise the wrapped iterators will dangle.
*/
RangedIteratorWrapper (RangedValuesIterator<T> iteratorIn, RangedValuesIterator<T> endIn)
: iterator { std::move (iteratorIn) },
end { std::move (endIn) }
@ -938,6 +892,12 @@ private:
RangedValuesIterator<T> iterator, end;
};
template <typename Iterable>
[[nodiscard]] auto makeRangedIteratorWrapper (Iterable* iterable)
{
return RangedIteratorWrapper { iterable->begin(), iterable->end() };
}
/* A wrapper type encapsulating multiple RangedValues objects and providing iterator support.
The iterator will advance through Ranges that are intersections with homogeneous values in each
@ -967,15 +927,14 @@ template <typename... Values>
class IntersectingRangedValues
{
public:
static_assert (sizeof...(Values) > 0, "IntersectingRangedValues() must wrap at least one RangedValues object");
static_assert (sizeof... (Values) > 0, "IntersectingRangedValues() must wrap at least one RangedValues object");
/* This constructor takes a pointer rather than a reference to make it clearer that the pointed-to
objects must outlive the IntersectingRangedValues instance. Passing a pointer also makes
it harder to accidentally reference a temporary when constructing IntersectingRangedValues.
*/
template <typename... Iterables>
explicit IntersectingRangedValues (Iterables*... iterable)
: iteratorWrappers { std::make_tuple (RangedIteratorWrapper<Values> { iterable->begin(), iterable->end() }...) }
explicit IntersectingRangedValues (RangedIteratorWrapper<Values>... wrappers)
: iteratorWrappers { wrappers... }
{
}
@ -1094,7 +1053,7 @@ template <typename... Iterables>
{
static_assert (sizeof...(Iterables) > 0, "makeIntersectingRangedValues() requires at least one parameter");
return IntersectingRangedValues<std::remove_reference_t<decltype (std::declval<Iterables>().begin()->value)>...> { iterables... };
return IntersectingRangedValues (makeRangedIteratorWrapper (iterables)...);
}
} // namespace juce::detail

View file

@ -59,6 +59,11 @@ public:
return simpleShapedText.getNumGlyphs();
}
const detail::RangedValues<LineMetrics>& getLinesMetrics() const
{
return justifiedText.getLinesMetrics();
}
auto& getText() const
{
return text;
@ -74,9 +79,14 @@ public:
return justifiedText.getGlyphIndexAt (p);
}
auto getGlyphRanges (Range<int64> textRange) const
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const
{
return simpleShapedText.getGlyphRanges (textRange);
simpleShapedText.getGlyphRanges (textRange, outRanges);
}
RectangleList<float> getGlyphsBounds (Range<int64> glyphRange) const
{
return justifiedText.getGlyphsBounds (glyphRange);
}
auto getGlyphAnchor (int64 index) const
@ -129,6 +139,11 @@ int64 ShapedText::getNumGlyphs() const
return impl->getNumGlyphs();
}
const detail::RangedValues<LineMetrics>& ShapedText::getLinesMetrics() const
{
return impl->getLinesMetrics();
}
const String& ShapedText::getText() const
{
return impl->getText();
@ -144,12 +159,17 @@ int64 ShapedText::getGlyphIndexAt (Point<float> p) const
return impl->getGlyphIndexAt (p);
}
std::vector<Range<int64>> ShapedText::getGlyphRanges (Range<int64> textRange) const
void ShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const
{
return impl->getGlyphRanges (textRange);
return impl->getGlyphRanges (textRange, outRanges);
}
Point<float> ShapedText::getGlyphAnchor (int64 index) const
RectangleList<float> ShapedText::getGlyphsBounds (Range<int64> glyphRange) const
{
return impl->getGlyphsBounds (glyphRange);
}
GlyphAnchorResult ShapedText::getGlyphAnchor (int64 index) const
{
return impl->getGlyphAnchor (index);
}

View file

@ -61,10 +61,12 @@ public:
int64 getGlyphIndexAt (Point<float> p) const;
std::vector<Range<int64>> getGlyphRanges (Range<int64> textRange) const;
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const;
RectangleList<float> getGlyphsBounds (Range<int64> glyphRange) const;
/* @see JustifiedText::getGlyphAnchor() */
Point<float> getGlyphAnchor (int64 index) const;
GlyphAnchorResult getGlyphAnchor (int64 index) const;
/* Returns the widths for each line, that the glyphs would require to be rendered without being
truncated. This will or will not include the space required by trailing whitespaces in the
@ -92,6 +94,8 @@ public:
int64 getNumGlyphs() const;
const detail::RangedValues<LineMetrics>& getLinesMetrics() const;
/* @internal */
const JustifiedText& getJustifiedText() const;

View file

@ -510,14 +510,19 @@ static detail::RangedValues<Font> findSuitableFontsForText (const Font& font,
const String& language = {})
{
detail::RangedValues<std::optional<Font>> fonts;
fonts.set ({ 0, (int64) text.length() }, font);
detail::Ranges::Operations ops;
fonts.set ({ 0, (int64) text.length() }, font, ops);
ops.clear();
const auto getResult = [&]
{
detail::RangedValues<Font> result;
for (const auto [r, v] : fonts)
result.set (r, v.value_or (font));
{
result.set (r, v.value_or (font), ops);
ops.clear();
}
return result;
};
@ -542,7 +547,10 @@ static detail::RangedValues<Font> findSuitableFontsForText (const Font& font,
}
for (const auto i : fontNotFound)
fonts.set ({ i, i + 1 }, std::nullopt);
{
fonts.set ({ i, i + 1 }, std::nullopt, ops);
ops.clear();
}
return fontNotFound.size();
};
@ -564,7 +572,10 @@ static detail::RangedValues<Font> findSuitableFontsForText (const Font& font,
}
for (const auto& c : changes)
fonts.set (c.first, c.second);
{
fonts.set (c.first, c.second, ops);
ops.clear();
}
if (const auto newNumMissingGlyphs = markMissingGlyphs();
std::exchange (numMissingGlyphs, newNumMissingGlyphs) == newNumMissingGlyphs)
@ -580,6 +591,7 @@ static detail::RangedValues<Font> findSuitableFontsForText (const Font& font,
static RangedValues<Font> resolveFontsWithFallback (const String& string, const RangedValues<Font>& fonts)
{
RangedValues<Font> resolved;
detail::Ranges::Operations ops;
for (const auto [r, f] : fonts)
{
@ -587,7 +599,10 @@ static RangedValues<Font> resolveFontsWithFallback (const String& string, const
(int) std::min (r.getEnd(), (int64) string.length())));
for (const auto [subRange, font] : rf)
resolved.set<MergeEqualItems::no> (subRange + r.getStart(), font);
{
resolved.set (subRange + r.getStart(), font, ops, MergeEqualItemsNo{});
ops.clear();
}
}
return resolved;
@ -801,8 +816,10 @@ private:
template <typename T>
static auto rangedValuesWithOffset (detail::RangedValues<T> rv, int64 offset = 0)
{
rv.shift (std::numeric_limits<int64>::min(), -offset);
rv.eraseUpTo (0);
detail::Ranges::Operations ops;
rv.shift (std::numeric_limits<int64>::min(), -offset, ops);
ops.clear();
rv.eraseUpTo (0, ops);
return rv;
}
@ -831,6 +848,8 @@ struct Shaper
rangedValuesWithOffset (options.getFontsForRange(),
shapingRange.getStart()));
detail::Ranges::Operations ops;
for (Unicode::LineBreakIterator lineIter { makeSpan (analysis) }; auto lineRun = lineIter.next();)
{
for (Unicode::ScriptRunIterator scriptIter { *lineRun }; auto scriptRun = scriptIter.next();)
@ -850,11 +869,14 @@ struct Shaper
for (const auto [range, font] : fonts.getIntersectionsWith (bidiRange))
{
shaperRuns.set<MergeEqualItems::no> (range,
{ scriptRun->front().script,
options.getLanguage(),
*it,
font });
shaperRuns.set (range,
{ scriptRun->front().script,
options.getLanguage(),
*it,
font },
ops,
MergeEqualItemsNo{});
ops.clear();
}
it = next;
@ -889,6 +911,8 @@ struct Shaper
return *it;
});
detail::Ranges::Operations ops;
if (! shapedGlyphs.getRanges().covers ({ startFrom, nextSoftBreakBefore }))
{
for (auto it = shaperRuns.find (startFrom);
@ -905,12 +929,15 @@ struct Shaper
it->value.language,
it->value.embeddingLevel);
shapedGlyphs.set<MergeEqualItems::no> (shapingRange,
{
std::make_shared<std::vector<ShapedGlyph>> (std::move (g)),
it->value.embeddingLevel % 2 == 0,
it->value.resolvedFont
});
shapedGlyphs.set (shapingRange,
{
std::make_shared<std::vector<ShapedGlyph>> (std::move (g)),
it->value.embeddingLevel % 2 == 0,
it->value.resolvedFont
},
ops,
MergeEqualItemsNo{});
ops.clear();
}
}
@ -1030,7 +1057,7 @@ public:
return nextState.isEmpty()
|| glyph.advance.getX() <= remainingWidth
|| (nextState.trailingWhitespaceCanExtendBeyondMargin
&& glyph.whitespace
&& glyph.isWhitespace()
&& nextState.isInTrailingPosition (glyph));
});
@ -1190,6 +1217,8 @@ static void foldLinesBeyondLineLimit (std::vector<std::vector<WrappedGlyphsCurso
void SimpleShapedText::shape (const String& data,
const ShapedTextOptions& options)
{
detail::Ranges::Operations ops;
for (const auto& lineRange : getLineRanges (data))
{
Shaper shaper { data, lineRange, options };
@ -1234,12 +1263,15 @@ void SimpleShapedText::shape (const String& data,
for (auto i = start; i < end; ++i)
glyphsInVisualOrder[(size_t) i].cluster += lineRange.getStart();
glyphLookup.set<MergeEqualItems::no> (s.textRange + lineRange.getStart(), { { start, end }, ltr });
resolvedFonts.set ({ start, end }, s.font);
glyphLookup.set (s.textRange + lineRange.getStart(), { { start, end }, ltr }, ops, MergeEqualItemsNo{});
ops.clear();
resolvedFonts.set ({ start, end }, s.font, ops);
ops.clear();
}
const auto lineEnd = (int64) glyphsInVisualOrder.size();
lineNumbers.set ({ lineStart, lineEnd}, (int64) lineNumbers.size());
lineNumbers.set ({ lineStart, lineEnd}, (int64) lineNumbers.size(), ops);
ops.clear();
}
}
}
@ -1307,8 +1339,12 @@ Range<int64> SimpleShapedText::getTextRange (int64 glyphIndex) const
return Range<int64>::withStartAndLength (cluster, std::max ((int64) 1, nextAdjacentCluster - cluster));
}
/* Returns the last element that is smaller than value or equal to it. Returns begin of no such
element is found.
/* 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.
Returns end otherwise.
NB: lower_bound: equal or greater
upper_bound: greater
@ -1333,9 +1369,11 @@ auto lessThanOrEqual (It begin, It end, Value v, Callback extractValue)
return it;
}
std::vector<Range<int64>> SimpleShapedText::getGlyphRanges (Range<int64> textRange) const
void SimpleShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const
{
Ranges glyphRanges;
outRanges.clear();
Ranges glyphRanges { std::move (outRanges) };
detail::Ranges::Operations ops;
for (const auto is : glyphLookup.getIntersectionsWith (textRange))
{
@ -1361,7 +1399,7 @@ std::vector<Range<int64>> SimpleShapedText::getGlyphRanges (Range<int64> textRan
if (subRangeLookup.ltr)
{
glyphRanges.set (getGlyphSubRange (glyphs.begin(), glyphs.end()) + glyphsSubRange.getStart());
glyphRanges.set (getGlyphSubRange (glyphs.begin(), glyphs.end()) + glyphsSubRange.getStart(), ops);
}
else
{
@ -1369,11 +1407,14 @@ std::vector<Range<int64>> SimpleShapedText::getGlyphRanges (Range<int64> textRan
std::reverse_iterator { glyphs.begin() });
glyphRanges.set ({ glyphsSubRange.getEnd() - reverseRange.getEnd(),
glyphsSubRange.getEnd() - reverseRange.getStart() });
glyphsSubRange.getEnd() - reverseRange.getStart() },
ops);
}
ops.clear();
}
return glyphRanges.getRanges();
outRanges = std::move (glyphRanges.getRanges());
}
#if JUCE_UNIT_TESTS

View file

@ -86,7 +86,8 @@ public:
[[nodiscard]] ShapedTextOptions withFont (Font x) const
{
RangedValues<Font> fonts;
fonts.set ({ 0, std::numeric_limits<int64>::max() }, x);
detail::Ranges::Operations ops;
fonts.set ({ 0, std::numeric_limits<int64>::max() }, x, ops);
return withMember (*this, &ShapedTextOptions::fontsForRange, std::move (fonts));
}
@ -177,7 +178,8 @@ private:
detail::RangedValues<Font> fontsForRange = std::invoke ([&]
{
detail::RangedValues<Font> result;
result.set ({ 0, std::numeric_limits<int64>::max() }, FontOptions { 15.0f });
detail::Ranges::Operations ops;
result.set ({ 0, std::numeric_limits<int64>::max() }, FontOptions { 15.0f }, ops);
return result;
});
@ -194,13 +196,35 @@ private:
struct ShapedGlyph
{
uint32_t glyphId;
int64 cluster;
bool unsafeToBreak;
bool whitespace;
bool newline;
ShapedGlyph (uint32_t glyphIdIn,
int64 clusterIn,
bool unsafeToBreakIn,
bool whitespaceIn,
bool newlineIn,
Point<float> advanceIn,
Point<float> offsetIn)
: advance (advanceIn),
offset (offsetIn),
cluster (clusterIn),
glyphId (glyphIdIn),
unsafeToBreak (unsafeToBreakIn),
whitespace (whitespaceIn),
newline (newlineIn) {}
bool isUnsafeToBreak() const { return unsafeToBreak; }
bool isWhitespace() const { return whitespace; }
bool isNewline() const { return newline; }
Point<float> advance;
Point<float> offset;
int64 cluster;
uint32_t glyphId;
private:
// These are effectively bools, pack into a single int once we have more than four flags.
int8_t unsafeToBreak;
int8_t whitespace;
int8_t newline;
};
struct GlyphLookupEntry
@ -227,7 +251,7 @@ public:
Range<int64> getTextRange (int64 glyphIndex) const;
std::vector<Range<int64>> getGlyphRanges (Range<int64> textRange) const;
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const;
int64 getNumLines() const { return (int64) lineNumbers.getRanges().size(); }
int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); }

View file

@ -188,7 +188,7 @@ static void addGlyphsFromShapedText (GlyphArrangement& ga, const detail::ShapedT
position.getX() + x,
position.getY() + y,
glyph.advance.getX(),
glyph.whitespace };
glyph.isWhitespace() };
ga.addGlyph (std::move (pg));
}

View file

@ -397,6 +397,8 @@ void TextLayout::createStandardLayout (const AttributedString& text)
{
using namespace detail;
detail::Ranges::Operations ops;
RangedValues<Font> fonts;
RangedValues<Colour> colours;
@ -404,8 +406,9 @@ void TextLayout::createStandardLayout (const AttributedString& text)
{
const auto& attribute = text.getAttribute (i);
const auto range = castTo<int64> (attribute.range);
fonts.set (range, attribute.font);
colours.set (range, attribute.colour);
fonts.set (range, attribute.font, ops);
colours.set (range, attribute.colour, ops);
ops.clear();
}
auto shapedTextOptions = ShapedTextOptions{}.withFonts (fonts)
@ -457,7 +460,7 @@ void TextLayout::createStandardLayout (const AttributedString& text)
for (auto it = std::reverse_iterator { glyphs.end() },
end = std::reverse_iterator { glyphs.begin() };
it != end && it->whitespace;
it != end && it->isWhitespace();
++it)
{
--i;