From bc093fa64cecf7b2ae2f7754b2eb4d5a1a94720e Mon Sep 17 00:00:00 2001 From: attila Date: Wed, 5 Mar 2025 18:58:20 +0100 Subject: [PATCH] Ranges: ShapedText: Use out params for improving TextEditor performance --- .../detail/juce_JustifiedText.cpp | 127 ++++++-- .../juce_graphics/detail/juce_JustifiedText.h | 25 +- modules/juce_graphics/detail/juce_Ranges.cpp | 206 +++++++------ modules/juce_graphics/detail/juce_Ranges.h | 283 ++++++++---------- .../juce_graphics/detail/juce_ShapedText.cpp | 30 +- .../juce_graphics/detail/juce_ShapedText.h | 8 +- .../detail/juce_SimpleShapedText.cpp | 99 ++++-- .../detail/juce_SimpleShapedText.h | 40 ++- .../fonts/juce_GlyphArrangement.cpp | 2 +- .../juce_graphics/fonts/juce_TextLayout.cpp | 9 +- 10 files changed, 494 insertions(+), 335 deletions(-) diff --git a/modules/juce_graphics/detail/juce_JustifiedText.cpp b/modules/juce_graphics/detail/juce_JustifiedText.cpp index eb91cf6568..b2463b7352 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.cpp +++ b/modules/juce_graphics/detail/juce_JustifiedText.cpp @@ -44,7 +44,7 @@ static int64 getNumLeadingWhitespaces (Span 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 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 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 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 glyphs, bool trailin struct MainAxisLineAlignment { - float anchor{}, extraWhitespaceAdvance{}; + float anchor{}, extraWhitespaceAdvance{}, effectiveLineLength; Range 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 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 (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::withStartAndLength (lastLineVisibleRange.getStart() - 1, 1), DrawType::ellipsis); + rangesToDraw.set (Range::withStartAndLength (lastLineVisibleRange.getStart() - 1, 1), DrawType::ellipsis, ops); else - rangesToDraw.set (Range::withStartAndLength (lastLineVisibleRange.getEnd(), 1), DrawType::ellipsis); + rangesToDraw.set (Range::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 p) const @@ -476,8 +496,8 @@ int64 JustifiedText::getGlyphIndexAt (Point 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 p) const return glyphIndex; } -Point JustifiedText::getGlyphAnchor (int64 index) const +GlyphAnchorResult JustifiedText::getGlyphAnchor (int64 index) const { jassert (index >= 0); @@ -499,19 +519,70 @@ Point 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 JustifiedText::getGlyphsBounds (Range glyphRange) const +{ + RectangleList bounds; + + if (linesMetrics.isEmpty()) + return bounds; + + const auto getBounds = [&] (const LineMetrics& line, int64 lineStart, int64 boundsStart, int64 boundsEnd) -> Rectangle + { + 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()) diff --git a/modules/juce_graphics/detail/juce_JustifiedText.h b/modules/juce_graphics/detail/juce_JustifiedText.h index 7afd5a9e02..fa34ff544b 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.h +++ b/modules/juce_graphics/detail/juce_JustifiedText.h @@ -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 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 getGlyphAnchor (int64 index) const; + GlyphAnchorResult getGlyphAnchor (int64 glyphIndex) const; + + RectangleList getGlyphsBounds (Range 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 linesMetrics; diff --git a/modules/juce_graphics/detail/juce_Ranges.cpp b/modules/juce_graphics/detail/juce_Ranges.cpp index fda3bcb405..60b7c35f85 100644 --- a/modules/juce_graphics/detail/juce_Ranges.cpp +++ b/modules/juce_graphics/detail/juce_Ranges.cpp @@ -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 (&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 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 ({ 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 ({ 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 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 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 ({ Range::withStartAndLength (beginInsertionAt, numElemsToInsert) }, - 'a' + (char) random.nextInt (25)); + rangedValuesNotMerged.insert ({ Range::withStartAndLength (beginInsertionAt, numElemsToInsert) }, + 'a' + (char) random.nextInt (25), + ops, + MergeEqualItemsNo{}); expectEquals (getCumulativeRangeLengths (rangedValuesNotMerged) - totalLengthBeforeInsert, numElemsToInsert); - rangedValuesMerged.insert ({ Range::withStartAndLength (beginInsertionAt, numElemsToInsert) }, - 'a' + (char) random.nextInt (25)); + rangedValuesMerged.insert ({ Range::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 ({ 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 ({ 2, 4 }, 'a'); + rangedValues.insert ({ 2, 4 }, 'a', ops, MergeEqualItemsNo{}); expectEquals ((int64) rangedValues.size(), (int64) 5); @@ -647,16 +659,18 @@ public: { RangedValues emptyRangedValues; - auto ops = emptyRangedValues.insert ({ 0, 0 }, 'a'); + ops.clear(); + emptyRangedValues.insert ({ 0, 0 }, 'a', ops); expect (emptyRangedValues.isEmpty()); expect (ops.empty()); } { RangedValues 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 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 ({ 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 ({ 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 ({ 20, 30 }, 'b'); + rangedValues.set ({ 20, 30 }, 'b', ops, MergeEqualItemsNo{}); - rangedValues.drop ({ 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 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 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 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; diff --git a/modules/juce_graphics/detail/juce_Ranges.h b/modules/juce_graphics/detail/juce_Ranges.h index 50261705ec..bc9fed087d 100644 --- a/modules/juce_graphics/detail/juce_Ranges.h +++ b/modules/juce_graphics/detail/juce_Ranges.h @@ -106,20 +106,6 @@ struct Ranges final using Operations = std::vector; - 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> 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 r) + void erase (Range 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 r) + void drop (Range 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 newRange) + void set (Range 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 newRange) + void insert (Range 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 constexpr auto hasEqualityOperator = false; @@ -469,13 +421,13 @@ private: using InternalIterator = const Range*; public: - using value_type = std::pair, T>; + using value_type = RangedValuesIteratorItem; using difference_type = std::ptrdiff_t; - using reference = RangedValuesIteratorItem; + 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; @@ -625,70 +580,84 @@ public: const T& value; }; - //============================================================================== - // Basic operations - template - auto set (Range r, T v) + template + 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 (offset)); + + return Span { start, newSize }; } - template - auto insert (Range r, T v) + //============================================================================== + // Basic operations + template + void set (Range r, T v, Ranges::Operations& ops, MergeEquals = {}) { - static_assert (mergeEquals == MergeEqualItems::no || canMergeEqualItems, + static_assert (std::is_same_v || 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) { - 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 + void insert (Range r, T v, Ranges::Operations& ops, MergeEquals = {}) + { + static_assert (std::is_same_v || 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) + { + 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 r) + void erase (Range 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 - auto drop (Range r) + template + void drop (Range r, Ranges::Operations& ops, MergeEquals = {}) { - static_assert (mergeEquals == MergeEqualItems::no || canMergeEqualItems, + static_assert (std::is_same_v || 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) + 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 - 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 (range, value)); - } - - return ops; + erase ({ ranges.get (0).getStart(), i }, ops); } auto getItemWithEnclosingRange (int64 i) @@ -802,11 +756,14 @@ public: RangedValues result; + detail::Ranges::Operations ops; + for (const auto& is : intersections) { auto valueIndex = ranges.getIndexForEnclosingRange (is.getStart()); jassert (valueIndex.has_value()); - result.template set (is, values[*valueIndex]); + result.set (is, values[*valueIndex], ops, MergeEqualItemsNo{}); + ops.clear(); } return result; @@ -814,8 +771,9 @@ public: RangedValues getIntersectionsStartingAtZeroWith (Range 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 ops) { for (const auto& op : ops) applyOperation (op); } - void applyOperations (const Ranges::Operations& ops, T v) + void applyOperations (Span ops, T v) { for (const auto& op : ops) applyOperation (op, v); @@ -918,9 +875,6 @@ template 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 iteratorIn, RangedValuesIterator endIn) : iterator { std::move (iteratorIn) }, end { std::move (endIn) } @@ -938,6 +892,12 @@ private: RangedValuesIterator iterator, end; }; +template +[[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 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 - explicit IntersectingRangedValues (Iterables*... iterable) - : iteratorWrappers { std::make_tuple (RangedIteratorWrapper { iterable->begin(), iterable->end() }...) } + explicit IntersectingRangedValues (RangedIteratorWrapper... wrappers) + : iteratorWrappers { wrappers... } { } @@ -1094,7 +1053,7 @@ template { static_assert (sizeof...(Iterables) > 0, "makeIntersectingRangedValues() requires at least one parameter"); - return IntersectingRangedValues().begin()->value)>...> { iterables... }; + return IntersectingRangedValues (makeRangedIteratorWrapper (iterables)...); } } // namespace juce::detail diff --git a/modules/juce_graphics/detail/juce_ShapedText.cpp b/modules/juce_graphics/detail/juce_ShapedText.cpp index 7279c6b471..1a26f5025e 100644 --- a/modules/juce_graphics/detail/juce_ShapedText.cpp +++ b/modules/juce_graphics/detail/juce_ShapedText.cpp @@ -59,6 +59,11 @@ public: return simpleShapedText.getNumGlyphs(); } + const detail::RangedValues& getLinesMetrics() const + { + return justifiedText.getLinesMetrics(); + } + auto& getText() const { return text; @@ -74,9 +79,14 @@ public: return justifiedText.getGlyphIndexAt (p); } - auto getGlyphRanges (Range textRange) const + void getGlyphRanges (Range textRange, std::vector>& outRanges) const { - return simpleShapedText.getGlyphRanges (textRange); + simpleShapedText.getGlyphRanges (textRange, outRanges); + } + + RectangleList getGlyphsBounds (Range 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& ShapedText::getLinesMetrics() const +{ + return impl->getLinesMetrics(); +} + const String& ShapedText::getText() const { return impl->getText(); @@ -144,12 +159,17 @@ int64 ShapedText::getGlyphIndexAt (Point p) const return impl->getGlyphIndexAt (p); } -std::vector> ShapedText::getGlyphRanges (Range textRange) const +void ShapedText::getGlyphRanges (Range textRange, std::vector>& outRanges) const { - return impl->getGlyphRanges (textRange); + return impl->getGlyphRanges (textRange, outRanges); } -Point ShapedText::getGlyphAnchor (int64 index) const +RectangleList ShapedText::getGlyphsBounds (Range glyphRange) const +{ + return impl->getGlyphsBounds (glyphRange); +} + +GlyphAnchorResult ShapedText::getGlyphAnchor (int64 index) const { return impl->getGlyphAnchor (index); } diff --git a/modules/juce_graphics/detail/juce_ShapedText.h b/modules/juce_graphics/detail/juce_ShapedText.h index e5024677aa..de69c6c10a 100644 --- a/modules/juce_graphics/detail/juce_ShapedText.h +++ b/modules/juce_graphics/detail/juce_ShapedText.h @@ -61,10 +61,12 @@ public: int64 getGlyphIndexAt (Point p) const; - std::vector> getGlyphRanges (Range textRange) const; + void getGlyphRanges (Range textRange, std::vector>& outRanges) const; + + RectangleList getGlyphsBounds (Range glyphRange) const; /* @see JustifiedText::getGlyphAnchor() */ - Point 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& getLinesMetrics() const; + /* @internal */ const JustifiedText& getJustifiedText() const; diff --git a/modules/juce_graphics/detail/juce_SimpleShapedText.cpp b/modules/juce_graphics/detail/juce_SimpleShapedText.cpp index 4ae2b4196a..ffd5ff5c60 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.cpp +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.cpp @@ -510,14 +510,19 @@ static detail::RangedValues findSuitableFontsForText (const Font& font, const String& language = {}) { detail::RangedValues> 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 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 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 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 findSuitableFontsForText (const Font& font, static RangedValues resolveFontsWithFallback (const String& string, const RangedValues& fonts) { RangedValues resolved; + detail::Ranges::Operations ops; for (const auto [r, f] : fonts) { @@ -587,7 +599,10 @@ static RangedValues resolveFontsWithFallback (const String& string, const (int) std::min (r.getEnd(), (int64) string.length()))); for (const auto [subRange, font] : rf) - resolved.set (subRange + r.getStart(), font); + { + resolved.set (subRange + r.getStart(), font, ops, MergeEqualItemsNo{}); + ops.clear(); + } } return resolved; @@ -801,8 +816,10 @@ private: template static auto rangedValuesWithOffset (detail::RangedValues rv, int64 offset = 0) { - rv.shift (std::numeric_limits::min(), -offset); - rv.eraseUpTo (0); + detail::Ranges::Operations ops; + rv.shift (std::numeric_limits::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 (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 (shapingRange, - { - std::make_shared> (std::move (g)), - it->value.embeddingLevel % 2 == 0, - it->value.resolvedFont - }); + shapedGlyphs.set (shapingRange, + { + std::make_shared> (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 (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 SimpleShapedText::getTextRange (int64 glyphIndex) const return Range::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> SimpleShapedText::getGlyphRanges (Range textRange) const +void SimpleShapedText::getGlyphRanges (Range textRange, std::vector>& 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> SimpleShapedText::getGlyphRanges (Range 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> SimpleShapedText::getGlyphRanges (Range 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 diff --git a/modules/juce_graphics/detail/juce_SimpleShapedText.h b/modules/juce_graphics/detail/juce_SimpleShapedText.h index bdc7146657..6bdcef5731 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.h +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.h @@ -86,7 +86,8 @@ public: [[nodiscard]] ShapedTextOptions withFont (Font x) const { RangedValues fonts; - fonts.set ({ 0, std::numeric_limits::max() }, x); + detail::Ranges::Operations ops; + fonts.set ({ 0, std::numeric_limits::max() }, x, ops); return withMember (*this, &ShapedTextOptions::fontsForRange, std::move (fonts)); } @@ -177,7 +178,8 @@ private: detail::RangedValues fontsForRange = std::invoke ([&] { detail::RangedValues result; - result.set ({ 0, std::numeric_limits::max() }, FontOptions { 15.0f }); + detail::Ranges::Operations ops; + result.set ({ 0, std::numeric_limits::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 advanceIn, + Point 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 advance; Point 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 getTextRange (int64 glyphIndex) const; - std::vector> getGlyphRanges (Range textRange) const; + void getGlyphRanges (Range textRange, std::vector>& outRanges) const; int64 getNumLines() const { return (int64) lineNumbers.getRanges().size(); } int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); } diff --git a/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp b/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp index b4376d919d..c51ec87c01 100644 --- a/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp +++ b/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp @@ -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)); } diff --git a/modules/juce_graphics/fonts/juce_TextLayout.cpp b/modules/juce_graphics/fonts/juce_TextLayout.cpp index 844f1dd049..083ef97c42 100644 --- a/modules/juce_graphics/fonts/juce_TextLayout.cpp +++ b/modules/juce_graphics/fonts/juce_TextLayout.cpp @@ -397,6 +397,8 @@ void TextLayout::createStandardLayout (const AttributedString& text) { using namespace detail; + detail::Ranges::Operations ops; + RangedValues fonts; RangedValues colours; @@ -404,8 +406,9 @@ void TextLayout::createStandardLayout (const AttributedString& text) { const auto& attribute = text.getAttribute (i); const auto range = castTo (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;