First Commit
This commit is contained in:
@@ -0,0 +1,627 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
void drawJustifiedText (const JustifiedText& text, const Graphics& g, AffineTransform);
|
||||
|
||||
//==============================================================================
|
||||
static constexpr auto maxWidthTolerance = 0.005f;
|
||||
|
||||
static int64 getNumLeadingWhitespaces (Span<const ShapedGlyph> glyphs)
|
||||
{
|
||||
const auto it = std::find_if_not (glyphs.begin(),
|
||||
glyphs.end(),
|
||||
[&] (const auto& g) { return g.isWhitespace(); });
|
||||
|
||||
return (int64) std::distance (glyphs.begin(), it);
|
||||
}
|
||||
|
||||
static int64 getNumTrailingWhitespaces (Span<const ShapedGlyph> glyphs)
|
||||
{
|
||||
if (glyphs.empty())
|
||||
return 0;
|
||||
|
||||
int64 trailingWhitespaces = 0;
|
||||
|
||||
for (auto it = glyphs.end(); --it >= glyphs.begin() && it->isWhitespace();)
|
||||
++trailingWhitespaces;
|
||||
|
||||
return trailingWhitespaces;
|
||||
}
|
||||
|
||||
struct NumWhitespaces
|
||||
{
|
||||
int64 total{}, leading{}, trailing{};
|
||||
};
|
||||
|
||||
static NumWhitespaces getNumWhitespaces (Span<const ShapedGlyph> glyphs)
|
||||
{
|
||||
const auto total = std::count_if (glyphs.begin(),
|
||||
glyphs.end(),
|
||||
[] (const auto& g) { return g.isWhitespace(); });
|
||||
|
||||
return { total, getNumLeadingWhitespaces (glyphs), getNumTrailingWhitespaces (glyphs) };
|
||||
}
|
||||
|
||||
struct LineLength
|
||||
{
|
||||
float total{}, withoutTrailingWhitespaces{};
|
||||
};
|
||||
|
||||
static LineLength getMainAxisLineLength (Span<const ShapedGlyph> glyphs)
|
||||
{
|
||||
const auto total = std::accumulate (glyphs.begin(),
|
||||
glyphs.end(),
|
||||
0.0f,
|
||||
[] (auto acc, const auto& g) { return acc + g.advance.getX(); });
|
||||
|
||||
auto trailingWhitespacesLength = 0.0f;
|
||||
|
||||
if (glyphs.empty())
|
||||
return {};
|
||||
|
||||
for (auto it = glyphs.end(); --it >= glyphs.begin() && it->isWhitespace();)
|
||||
trailingWhitespacesLength += it->advance.getX();
|
||||
|
||||
return { total, total - trailingWhitespacesLength };
|
||||
}
|
||||
|
||||
static float getMainAxisLineLength (Span<const ShapedGlyph> glyphs, bool trailingWhitespacesShouldFit)
|
||||
{
|
||||
const auto lengths = getMainAxisLineLength (glyphs);
|
||||
|
||||
return trailingWhitespacesShouldFit ? lengths.total : lengths.withoutTrailingWhitespaces;
|
||||
}
|
||||
|
||||
struct MainAxisLineAlignment
|
||||
{
|
||||
float anchor{}, extraWhitespaceAdvance{}, effectiveLineLength{};
|
||||
Range<int64> stretchableWhitespaces;
|
||||
};
|
||||
|
||||
static MainAxisLineAlignment getMainAxisLineAlignment (Justification justification,
|
||||
Span<const ShapedGlyph> glyphs,
|
||||
LineLength lineLength,
|
||||
std::optional<float> maxWidthOpt,
|
||||
std::optional<float> alignmentWidthOpt,
|
||||
bool trailingWhitespacesShouldFit)
|
||||
{
|
||||
const auto effectiveLineLength = (trailingWhitespacesShouldFit ? lineLength.total
|
||||
: lineLength.withoutTrailingWhitespaces);
|
||||
|
||||
const auto alignmentWidth = alignmentWidthOpt.value_or (maxWidthOpt.value_or (0.0f));
|
||||
const auto tooLong = alignmentWidth + maxWidthTolerance < effectiveLineLength;
|
||||
|
||||
MainAxisLineAlignment result;
|
||||
result.effectiveLineLength = effectiveLineLength;
|
||||
|
||||
// The alignment width opt is supporting the TextEditor use-case where all text remains visible
|
||||
// with scrolling, even if longer than the alignment width. We don't need to worry about the
|
||||
// front of and RTL text being visually truncated, because nothing is truncated.
|
||||
if (tooLong && alignmentWidthOpt.has_value())
|
||||
return result;
|
||||
|
||||
const auto mainAxisLineOffset = [&]
|
||||
{
|
||||
if (tooLong)
|
||||
{
|
||||
const auto approximateIsLeftToRight = [&]
|
||||
{
|
||||
if (glyphs.empty())
|
||||
return true;
|
||||
|
||||
return glyphs.front().cluster <= glyphs.back().cluster;
|
||||
}();
|
||||
|
||||
// We don't have to align LTR text, but we need to ensure that it's the logical back of
|
||||
// RTL text that falls outside the bounds.
|
||||
if (approximateIsLeftToRight)
|
||||
return 0.0f;
|
||||
|
||||
return alignmentWidth - effectiveLineLength;
|
||||
}
|
||||
|
||||
if (justification.testFlags (Justification::horizontallyCentred))
|
||||
{
|
||||
return (alignmentWidth - lineLength.withoutTrailingWhitespaces) / 2.0f;
|
||||
}
|
||||
|
||||
if (justification.testFlags (Justification::right))
|
||||
return alignmentWidth - effectiveLineLength;
|
||||
|
||||
return 0.0f;
|
||||
}();
|
||||
|
||||
const auto numWhitespaces = getNumWhitespaces (glyphs);
|
||||
|
||||
const auto stretchableWhitespaces = [&]() -> Range<int64>
|
||||
{
|
||||
if (! justification.testFlags (Justification::horizontallyJustified) || tooLong)
|
||||
return {};
|
||||
|
||||
return { numWhitespaces.leading, (int64) glyphs.size() - numWhitespaces.trailing };
|
||||
}();
|
||||
|
||||
const auto extraWhitespaceAdvance = [&]
|
||||
{
|
||||
if (! justification.testFlags (Justification::horizontallyJustified) || tooLong)
|
||||
return 0.0f;
|
||||
|
||||
const auto numWhitespacesBetweenWords = numWhitespaces.total
|
||||
- numWhitespaces.leading
|
||||
- numWhitespaces.trailing;
|
||||
|
||||
return numWhitespacesBetweenWords > 0 ? (alignmentWidth - effectiveLineLength) / (float) numWhitespacesBetweenWords
|
||||
: 0.0f;
|
||||
}();
|
||||
|
||||
return { mainAxisLineOffset, extraWhitespaceAdvance, effectiveLineLength, stretchableWhitespaces };
|
||||
}
|
||||
|
||||
struct LineInfo
|
||||
{
|
||||
float lineHeight{}, maxAscent{};
|
||||
MainAxisLineAlignment mainAxisLineAlignment;
|
||||
};
|
||||
|
||||
static float getCrossAxisStartingAnchor (Justification justification,
|
||||
Span<const LineInfo> lineInfos,
|
||||
std::optional<float> height,
|
||||
float leadingInHeight)
|
||||
{
|
||||
if (lineInfos.empty())
|
||||
return 0.0f;
|
||||
|
||||
const auto minimumTop = lineInfos.front().maxAscent + lineInfos.front().lineHeight * leadingInHeight;
|
||||
|
||||
if (! height.has_value())
|
||||
return minimumTop;
|
||||
|
||||
const auto textHeight = std::accumulate (lineInfos.begin(),
|
||||
lineInfos.end(),
|
||||
0.0f,
|
||||
[] (auto acc, const auto info) { return acc + info.lineHeight; });
|
||||
|
||||
if (justification.testFlags (Justification::verticallyCentred))
|
||||
return (*height - textHeight) / 2.0f + lineInfos.front().maxAscent;
|
||||
|
||||
if (justification.testFlags (Justification::bottom))
|
||||
{
|
||||
const auto bottomLeading = 0.5f * lineInfos.back().lineHeight * leadingInHeight;
|
||||
return *height - textHeight - bottomLeading + lineInfos.front().maxAscent;
|
||||
}
|
||||
|
||||
return minimumTop;
|
||||
}
|
||||
|
||||
JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions& options)
|
||||
: shapedText (*t)
|
||||
{
|
||||
const auto leading = options.getLeading() - 1.0f;
|
||||
|
||||
std::vector<LineInfo> lineInfos;
|
||||
|
||||
for (const auto [range, lineNumber] : shapedText.getLineNumbersForGlyphRanges())
|
||||
{
|
||||
// This is guaranteed by the RangedValues implementation. You can't assign a value to an
|
||||
// empty range.
|
||||
jassert (! range.isEmpty());
|
||||
|
||||
const auto fonts = shapedText.getResolvedFonts().getIntersectionsWith (range);
|
||||
|
||||
const auto lineHeight = std::accumulate (fonts.begin(),
|
||||
fonts.end(),
|
||||
0.0f,
|
||||
[] (auto acc, const auto& rangedFont)
|
||||
{ return std::max (acc, rangedFont.value.getHeight()); });
|
||||
|
||||
const auto maxAscent = std::accumulate (fonts.begin(),
|
||||
fonts.end(),
|
||||
0.0f,
|
||||
[] (auto acc, const auto& rangedFont)
|
||||
{ return std::max (acc, rangedFont.value.getAscent()); });
|
||||
|
||||
const auto glyphs = shapedText.getGlyphs (range);
|
||||
const auto lineLength = getMainAxisLineLength (glyphs);
|
||||
|
||||
auto m = [&]
|
||||
{
|
||||
return getMainAxisLineAlignment (options.getJustification(),
|
||||
glyphs,
|
||||
lineLength,
|
||||
options.getMaxWidth(),
|
||||
options.getAlignmentWidth(),
|
||||
options.getTrailingWhitespacesShouldFit());
|
||||
}();
|
||||
|
||||
const auto containsHardBreak = shapedText.getCodepoint (range.getEnd() - 1) == 0xa
|
||||
|| shapedText.getCodepoint (range.getStart()) == 0xa;
|
||||
|
||||
if (containsHardBreak || lineNumber == shapedText.getLineNumbersForGlyphRanges().back().value)
|
||||
{
|
||||
m.extraWhitespaceAdvance = {};
|
||||
m.stretchableWhitespaces = {};
|
||||
}
|
||||
|
||||
lineInfos.push_back ({ lineHeight, maxAscent, std::move (m) });
|
||||
minimumRequiredWidthsForLine.push_back (options.getTrailingWhitespacesShouldFit() ? lineLength.total
|
||||
: lineLength.withoutTrailingWhitespaces);
|
||||
}
|
||||
|
||||
auto baseline = options.isBaselineAtZero() ? 0.0f
|
||||
: getCrossAxisStartingAnchor (options.getJustification(),
|
||||
lineInfos,
|
||||
options.getHeight(),
|
||||
leading);
|
||||
|
||||
detail::Ranges::Operations ops;
|
||||
|
||||
std::optional<float> top;
|
||||
|
||||
for (const auto [lineIndex, lineInfo] : enumerate (lineInfos))
|
||||
{
|
||||
const auto lineNumber = shapedText.getLineNumbersForGlyphRanges().getItem ((size_t) lineIndex);
|
||||
const auto range = lineNumber.range;
|
||||
|
||||
const auto maxDescent = lineInfo.lineHeight - lineInfo.maxAscent;
|
||||
const auto nextLineTop = baseline + (1.0f + leading) * maxDescent + options.getAdditiveLineSpacing();
|
||||
|
||||
if (! top.has_value())
|
||||
top = baseline - (1.0f + leading) * lineInfo.maxAscent;
|
||||
|
||||
lineMetricsForGlyphRange.set (range,
|
||||
{ lineNumber.value,
|
||||
{ lineInfo.mainAxisLineAlignment.anchor, baseline },
|
||||
lineInfo.maxAscent,
|
||||
lineInfo.lineHeight - lineInfo.maxAscent,
|
||||
lineInfo.mainAxisLineAlignment.effectiveLineLength
|
||||
+ lineInfo.mainAxisLineAlignment.extraWhitespaceAdvance,
|
||||
*top,
|
||||
nextLineTop },
|
||||
ops,
|
||||
MergeEqualItemsNo{});
|
||||
|
||||
whitespaceStretch.set (range, 0.0f, ops);
|
||||
const auto stretchRange = lineInfo.mainAxisLineAlignment.stretchableWhitespaces + range.getStart();
|
||||
|
||||
whitespaceStretch.set (stretchRange,
|
||||
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, ops);
|
||||
|
||||
//==============================================================================
|
||||
// Everything above this line should work well given none of the lines were too
|
||||
// long. When Options::getMaxNumLines() == 0 this is guaranteed by SimpleShapedText.
|
||||
// The remaining logic below is for supporting
|
||||
// GlyphArrangement::addFittedText() when the maximum number of lines is
|
||||
// constrained.
|
||||
if (lineMetricsForGlyphRange.isEmpty())
|
||||
return;
|
||||
|
||||
const auto lastLineMetrics = lineMetricsForGlyphRange.back();
|
||||
const auto lastLineGlyphRange = lastLineMetrics.range;
|
||||
const auto lastLineGlyphs = shapedText.getGlyphs (lastLineGlyphRange);
|
||||
const auto lastLineLengths = getMainAxisLineLength (lastLineGlyphs);
|
||||
|
||||
const auto effectiveLength = options.getTrailingWhitespacesShouldFit() ? lastLineLengths.total
|
||||
: lastLineLengths.withoutTrailingWhitespaces;
|
||||
|
||||
if (! options.getMaxWidth().has_value()
|
||||
|| effectiveLength <= *options.getMaxWidth() + maxWidthTolerance)
|
||||
return;
|
||||
|
||||
const auto cutoffAtFront = lastLineMetrics.value.anchor.getX() < 0.0f - maxWidthTolerance;
|
||||
|
||||
const auto getLastLineVisibleRange = [&] (float ellipsisLength)
|
||||
{
|
||||
const auto r = [&]() -> Range<int64>
|
||||
{
|
||||
if (cutoffAtFront)
|
||||
{
|
||||
auto length = lastLineLengths.total;
|
||||
|
||||
for (auto it = lastLineGlyphs.begin(); it < lastLineGlyphs.end(); ++it)
|
||||
{
|
||||
length -= it->advance.getX();
|
||||
|
||||
if (! options.getMaxWidth().has_value()
|
||||
|| *options.getMaxWidth() >= ellipsisLength + length)
|
||||
{
|
||||
return { (int64) std::distance (lastLineGlyphs.begin(), it) + 1,
|
||||
(int64) lastLineGlyphs.size() };
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto length = lastLineLengths.total;
|
||||
|
||||
for (auto it = lastLineGlyphs.end() - 1; it >= lastLineGlyphs.begin(); --it)
|
||||
{
|
||||
length -= it->advance.getX();
|
||||
|
||||
if (! options.getMaxWidth().has_value()
|
||||
|| *options.getMaxWidth() >= ellipsisLength + length)
|
||||
{
|
||||
return { 0, (int64) std::distance (lastLineGlyphs.begin(), it) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}();
|
||||
|
||||
return r.movedToStartAt (r.getStart() + lastLineGlyphRange.getStart());
|
||||
};
|
||||
|
||||
const auto lastLineVisibleRangeWithoutEllipsis = getLastLineVisibleRange (0.0f);
|
||||
|
||||
const auto eraseLastLineFromRangesToDraw = [&]
|
||||
{
|
||||
rangesToDraw.eraseFrom (lastLineGlyphRange.getStart(), ops);
|
||||
ops.clear();
|
||||
};
|
||||
|
||||
eraseLastLineFromRangesToDraw();
|
||||
rangesToDraw.set (lastLineVisibleRangeWithoutEllipsis, DrawType::normal, ops);
|
||||
ops.clear();
|
||||
|
||||
if (options.getEllipsis().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// More logic supporting using ellipses
|
||||
const auto fontForEllipsis = [&]
|
||||
{
|
||||
const auto lastLineFonts = shapedText.getResolvedFonts().getIntersectionsWith (lastLineGlyphRange);
|
||||
|
||||
if (cutoffAtFront)
|
||||
return lastLineFonts.front().value;
|
||||
|
||||
return lastLineFonts.back().value;
|
||||
}();
|
||||
|
||||
ellipsis.emplace (&options.getEllipsis(), ShapedTextOptions {}.withFont (fontForEllipsis));
|
||||
|
||||
const auto lastLineVisibleRange = getLastLineVisibleRange (getMainAxisLineLength (ellipsis->getGlyphs(),
|
||||
options.getTrailingWhitespacesShouldFit()));
|
||||
|
||||
eraseLastLineFromRangesToDraw();
|
||||
rangesToDraw.set (lastLineVisibleRange, DrawType::normal, ops);
|
||||
ops.clear();
|
||||
|
||||
if (cutoffAtFront)
|
||||
rangesToDraw.set (Range<int64>::withStartAndLength (lastLineVisibleRange.getStart() - 1, 1), DrawType::ellipsis, ops);
|
||||
else
|
||||
rangesToDraw.set (Range<int64>::withStartAndLength (lastLineVisibleRange.getEnd(), 1), DrawType::ellipsis, ops);
|
||||
|
||||
ops.clear();
|
||||
|
||||
const auto lineWithEllipsisGlyphs = [&]
|
||||
{
|
||||
std::vector<ShapedGlyph> result;
|
||||
|
||||
const auto pushEllipsisGlyphs = [&]
|
||||
{
|
||||
const auto& range = ellipsis->getGlyphs();
|
||||
result.insert (result.begin(), range.begin(), range.end());
|
||||
};
|
||||
|
||||
if (cutoffAtFront)
|
||||
pushEllipsisGlyphs();
|
||||
|
||||
const auto& range = shapedText.getGlyphs (lastLineVisibleRange);
|
||||
result.insert (result.end(), range.begin(), range.end());
|
||||
|
||||
if (! cutoffAtFront)
|
||||
pushEllipsisGlyphs();
|
||||
|
||||
return result;
|
||||
}();
|
||||
|
||||
const auto realign = [&]
|
||||
{
|
||||
return getMainAxisLineAlignment (options.getJustification(),
|
||||
lineWithEllipsisGlyphs,
|
||||
getMainAxisLineLength (lineWithEllipsisGlyphs),
|
||||
options.getMaxWidth(),
|
||||
options.getAlignmentWidth(),
|
||||
options.getTrailingWhitespacesShouldFit());
|
||||
}();
|
||||
|
||||
lastLineMetrics.value.anchor.setX (realign.anchor);
|
||||
whitespaceStretch.set (lastLineGlyphRange, 0.0f, ops);
|
||||
whitespaceStretch.set (realign.stretchableWhitespaces + lastLineVisibleRange.getStart(),
|
||||
realign.extraWhitespaceAdvance, ops);
|
||||
}
|
||||
|
||||
int64 JustifiedText::getGlyphIndexToTheRightOf (Point<float> p) const
|
||||
{
|
||||
auto lineIt = lineMetricsForGlyphRange.begin();
|
||||
float lineTop = 0.0f;
|
||||
|
||||
while (lineIt != lineMetricsForGlyphRange.end())
|
||||
{
|
||||
const auto nextLineTop = lineIt->value.nextLineTop;
|
||||
|
||||
if (lineTop <= p.getY() && p.getY() < nextLineTop)
|
||||
break;
|
||||
|
||||
lineTop = nextLineTop;
|
||||
++lineIt;
|
||||
}
|
||||
|
||||
if (lineIt == lineMetricsForGlyphRange.end())
|
||||
return 0;
|
||||
|
||||
const auto glyphsInLine = shapedText.getGlyphs (lineIt->range);
|
||||
|
||||
auto glyphIndex = lineIt->range.getStart();
|
||||
auto glyphX = lineIt->value.anchor.getX();
|
||||
|
||||
for (const auto& glyph : glyphsInLine)
|
||||
{
|
||||
if ( p.getX() < glyphX + glyph.advance.getX() / 2.0f
|
||||
|| glyph.isNewline()
|
||||
|| (glyphIndex - lineIt->range.getStart() == (int64) glyphsInLine.size() - 1 && glyph.isWhitespace()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
++glyphIndex;
|
||||
glyphX += glyph.advance.getX();
|
||||
}
|
||||
|
||||
return glyphIndex;
|
||||
}
|
||||
|
||||
GlyphAnchorResult JustifiedText::getGlyphAnchor (int64 index) const
|
||||
{
|
||||
jassert (index >= 0);
|
||||
|
||||
if (lineMetricsForGlyphRange.isEmpty())
|
||||
return {};
|
||||
|
||||
const auto lineItem = lineMetricsForGlyphRange.getItemWithEnclosingRange (index)
|
||||
.value_or (lineMetricsForGlyphRange.back());
|
||||
|
||||
const auto indexInLine = index - lineItem.range.getStart();
|
||||
|
||||
GlyphAnchorResult anchor { lineItem.value.anchor, lineItem.value.maxAscent, lineItem.value.maxDescent };
|
||||
|
||||
for (auto [i, glyph] : enumerate (shapedText.getGlyphs (lineItem.range), int64{}))
|
||||
{
|
||||
if (i == indexInLine)
|
||||
{
|
||||
anchor.anchor += glyph.offset;
|
||||
return anchor;
|
||||
}
|
||||
|
||||
anchor.anchor += glyph.advance;
|
||||
}
|
||||
|
||||
return anchor;
|
||||
}
|
||||
|
||||
RectangleList<float> JustifiedText::getGlyphsBounds (Range<int64> glyphRange) const
|
||||
{
|
||||
RectangleList<float> bounds;
|
||||
|
||||
if (lineMetricsForGlyphRange.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 = lineMetricsForGlyphRange.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 (lineMetricsForGlyphRange.isEmpty())
|
||||
return 0.0f;
|
||||
|
||||
return lineMetricsForGlyphRange.back().value.nextLineTop;
|
||||
}
|
||||
|
||||
void drawJustifiedText (const JustifiedText& text, const Graphics& g, AffineTransform transform)
|
||||
{
|
||||
auto& context = g.getInternalContext();
|
||||
context.saveState();
|
||||
const ScopeGuard restoreGraphicsContext { [&context] { context.restoreState(); } };
|
||||
|
||||
text.accessTogetherWith ([&] (auto glyphs, auto positions, auto font, auto, auto)
|
||||
{
|
||||
if (context.getFont() != font)
|
||||
context.setFont (font);
|
||||
|
||||
std::vector<uint16_t> glyphIds (glyphs.size());
|
||||
|
||||
std::transform (glyphs.begin(),
|
||||
glyphs.end(),
|
||||
glyphIds.begin(),
|
||||
[] (auto& glyph) { return (uint16_t) glyph.glyphId; });
|
||||
|
||||
context.drawGlyphs (glyphIds, positions, transform);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace juce::detail
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
template <size_t StartingAt, typename Tuple, size_t... Is>
|
||||
constexpr auto partiallyUnpackImpl (Tuple&& tuple, std::index_sequence<Is...>)
|
||||
{
|
||||
return std::tie (std::get<StartingAt + Is> (tuple)...);
|
||||
}
|
||||
|
||||
template <size_t StartingAt, size_t NumElems, typename Tuple>
|
||||
constexpr auto partiallyUnpack (Tuple&& tuple)
|
||||
{
|
||||
return partiallyUnpackImpl<StartingAt> (std::forward<Tuple> (tuple), std::make_index_sequence<NumElems>{});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct LineMetrics
|
||||
{
|
||||
int64 lineNumber;
|
||||
Point<float> anchor;
|
||||
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
|
||||
{
|
||||
private:
|
||||
enum class DrawType
|
||||
{
|
||||
normal,
|
||||
ellipsis
|
||||
};
|
||||
|
||||
public:
|
||||
JustifiedText (const SimpleShapedText* t, const ShapedTextOptions& options);
|
||||
|
||||
/* Provides access to the data stored in the ShapedText.
|
||||
|
||||
The provided callable will be called multiple times for "uniform glyph runs", for which all
|
||||
callback parameters are the same.
|
||||
|
||||
Between each subsequent callback at least one of the provided parameters will be different.
|
||||
|
||||
The callbacks happen in visual order i.e. left to right, which is irrespective of the
|
||||
underlying text's writing direction.
|
||||
|
||||
The callback parameters in order are:
|
||||
- the glyphs
|
||||
- the positions for each glyph in the previous parameter
|
||||
- the Font with which these glyphs should be rendered
|
||||
- the range in all glyphs this ShapedText object holds, that correspond to the current glyphs
|
||||
- a line number which increases by one for each new line
|
||||
- followed by any number of ValueType parameters for the supplied RangedValues<ValueType> arguments
|
||||
*/
|
||||
template <typename Callable, typename... RangedValues>
|
||||
void accessTogetherWith (Callable&& callback, RangedValues&&... rangedValues) const
|
||||
{
|
||||
std::optional<int64> lastLine;
|
||||
int64 lastGlyph = 0;
|
||||
Point<float> anchor {};
|
||||
|
||||
for (const auto item : makeIntersectingRangedValues (&shapedText.getResolvedFonts(),
|
||||
&lineMetricsForGlyphRange,
|
||||
&rangesToDraw,
|
||||
&whitespaceStretch,
|
||||
(&rangedValues)...))
|
||||
{
|
||||
const auto& [range, font, lineMetrics, drawType, stretch] = partiallyUnpack<0, 5> (item);
|
||||
const auto& rest = partiallyUnpack<5, std::tuple_size_v<decltype (item)> - 5> (item);
|
||||
|
||||
if (std::exchange (lastLine, lineMetrics.lineNumber) != lineMetrics.lineNumber)
|
||||
anchor = lineMetrics.anchor;
|
||||
|
||||
if (range.getStart() != lastGlyph && drawType != DrawType::ellipsis)
|
||||
{
|
||||
detail::RangedValues<int8_t> glyphMask;
|
||||
Ranges::Operations ops;
|
||||
|
||||
const auto firstGlyphInCurrentLine = shapedText.getLineNumbersForGlyphRanges().getItem ((size_t) lineMetrics.lineNumber)
|
||||
.range
|
||||
.getStart();
|
||||
|
||||
glyphMask.set ({ std::max (lastGlyph, firstGlyphInCurrentLine), range.getStart() }, 1, ops);
|
||||
|
||||
for (const auto [skippedRange, skippedStretch, _] : makeIntersectingRangedValues (&whitespaceStretch,
|
||||
&glyphMask))
|
||||
{
|
||||
ignoreUnused (_);
|
||||
|
||||
for (const auto& skippedGlyph : shapedText.getGlyphs (skippedRange))
|
||||
{
|
||||
anchor += skippedGlyph.advance;
|
||||
|
||||
if (skippedGlyph.isWhitespace())
|
||||
anchor.addXY (skippedStretch, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastGlyph = range.getEnd();
|
||||
|
||||
const auto glyphs = [this, r = range, dt = drawType]() -> Span<const ShapedGlyph>
|
||||
{
|
||||
if (dt == DrawType::ellipsis)
|
||||
return ellipsis->getGlyphs();
|
||||
|
||||
return shapedText.getGlyphs (r);
|
||||
}();
|
||||
|
||||
std::vector<Point<float>> positions (glyphs.size());
|
||||
|
||||
std::transform (glyphs.begin(),
|
||||
glyphs.end(),
|
||||
positions.begin(),
|
||||
[&anchor, &s = stretch] (auto& glyph)
|
||||
{
|
||||
auto result = anchor + glyph.offset;
|
||||
|
||||
anchor += glyph.advance;
|
||||
|
||||
if (glyph.isWhitespace())
|
||||
anchor.addXY (s, 0.0f);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const auto callbackFont =
|
||||
drawType == DrawType::ellipsis ? ellipsis->getResolvedFonts().front().value : font;
|
||||
const auto callbackParameters =
|
||||
std::tuple_cat (std::tie (glyphs, positions, callbackFont, range, lineMetrics), rest);
|
||||
|
||||
const auto invokeNullChecked = [&] (auto&... params)
|
||||
{ NullCheckedInvocation::invoke (callback, params...); };
|
||||
|
||||
std::apply (invokeNullChecked, callbackParameters);
|
||||
}
|
||||
}
|
||||
|
||||
/* This is how much cumulative widths glyphs take up in each line. Whether the trailing
|
||||
whitespace is included depends on the ShapedTextOptions::getWhitespaceShouldFitInLine()
|
||||
setting.
|
||||
*/
|
||||
auto& getMinimumRequiredWidthForLines() const { return minimumRequiredWidthsForLine; }
|
||||
|
||||
int64 getGlyphIndexToTheRightOf (Point<float> p) const;
|
||||
|
||||
/* If the passed in index parameter is greater than the index of the last contained glyph,
|
||||
then the returned anchor specifies the location where the next glyph would have to be
|
||||
placed i.e. lastGlyphAnchor + lastGlyphAdvance.
|
||||
*/
|
||||
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
|
||||
members of the ShapedTextOptions object.
|
||||
|
||||
This guarantees that if ShapedText object1 is drawn at y = 0 and object2 is drawn at
|
||||
y = object1.getHeight(), then the two texts will be spaced exactly as if they were a single
|
||||
ShapedText object containing both texts.
|
||||
*/
|
||||
float getHeight() const;
|
||||
|
||||
const auto& getLineMetricsForGlyphRange() const { return lineMetricsForGlyphRange; }
|
||||
|
||||
private:
|
||||
const SimpleShapedText& shapedText;
|
||||
detail::RangedValues<LineMetrics> lineMetricsForGlyphRange;
|
||||
std::optional<SimpleShapedText> ellipsis;
|
||||
detail::RangedValues<DrawType> rangesToDraw;
|
||||
detail::RangedValues<float> whitespaceStretch;
|
||||
std::vector<float> minimumRequiredWidthsForLine;
|
||||
};
|
||||
|
||||
} // namespace juce::detail
|
||||
@@ -0,0 +1,871 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
template <typename T, typename = void>
|
||||
constexpr auto hasGetStartFunction = false;
|
||||
|
||||
template <typename T>
|
||||
constexpr auto hasGetStartFunction<T, std::void_t<decltype (std::declval<T>().getStart())>> = true;
|
||||
|
||||
template <typename RangeType, typename std::enable_if<hasGetStartFunction<RangeType>, int>::type = 0>
|
||||
std::ostream& operator<< (std::ostream& os, const RangeType& range)
|
||||
{
|
||||
os << "[" << range.getStart() << ", " << range.getEnd() << ")";
|
||||
return os;
|
||||
}
|
||||
|
||||
static String& operator<< (String& s, Range<int64> r)
|
||||
{
|
||||
return s += "[" + String { r.getStart() } + ", " + String { r.getEnd() } + ")";
|
||||
}
|
||||
|
||||
template <typename ValueType>
|
||||
static auto getCumulativeRangeLengths (const RangedValues<ValueType>& rv)
|
||||
{
|
||||
int64 totalLength{};
|
||||
|
||||
for (size_t i = 0; i < rv.size(); ++i)
|
||||
totalLength += rv.getItem (i).range.getLength();
|
||||
|
||||
return totalLength;
|
||||
}
|
||||
|
||||
template <typename ValueType>
|
||||
static auto toString (const RangedValues<ValueType>& rv)
|
||||
{
|
||||
String s {};
|
||||
|
||||
for (size_t i = 0; i < rv.size(); ++i)
|
||||
{
|
||||
auto item = rv.getItem (i);
|
||||
s << item.range << ": " << item.value << "\n";
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
class RangesTestsBase : public UnitTest
|
||||
{
|
||||
public:
|
||||
using UnitTest::UnitTest;
|
||||
|
||||
void expectRange (Range<int64> actual, Range<int64> expected)
|
||||
{
|
||||
String failureMessage { "range " };
|
||||
failureMessage << actual << " did not equal expected range " << expected;
|
||||
expect (actual == expected, failureMessage);
|
||||
}
|
||||
};
|
||||
|
||||
class RangesTests : public RangesTestsBase
|
||||
{
|
||||
public:
|
||||
RangesTests() : RangesTestsBase ("Ranges", UnitTestCategories::containers) {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
Ranges::Operations ops;
|
||||
|
||||
beginTest ("Ranges::set() - basics");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ranges.set ({ -3, 14 }, ops);
|
||||
expectRange (ranges.get (0), { -3, 14 });
|
||||
|
||||
ranges.set ({ 7, 20 }, ops);
|
||||
expectRange (ranges.get (0), { -3, 7 });
|
||||
expectRange (ranges.get (1), { 7, 20 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::set() - neighbouring ranges extents are modified");
|
||||
{
|
||||
Ranges ranges;
|
||||
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);
|
||||
expectRange (ranges.get (0), { -3, 10 });
|
||||
expectRange (ranges.get (1), { 10, 25 });
|
||||
expectRange (ranges.get (2), { 25, 30 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::set() - setting a range inside another one splits that range");
|
||||
{
|
||||
Ranges ranges;
|
||||
ranges.set ({ -3, 14 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 1);
|
||||
|
||||
//==============================================================================
|
||||
ranges.set ({ 3, 7 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 3);
|
||||
expectRange (ranges.get (0), { -3, 3 });
|
||||
expectRange (ranges.get (1), { 3, 7 });
|
||||
expectRange (ranges.get (2), { 7, 14 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::set() - old ranges falling within the bounds of a newly set are erased");
|
||||
{
|
||||
Ranges ranges;
|
||||
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 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 3);
|
||||
expectRange (ranges.get (0), { 0, 4 });
|
||||
expectRange (ranges.get (1), { 4, 36 });
|
||||
expectRange (ranges.get (2), { 36, 50 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::set() - setting an empty range should be a no-op");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ops.clear();
|
||||
ranges.set ({ 0, 0 }, ops);
|
||||
expect (ranges.isEmpty());
|
||||
expect (ops.empty());
|
||||
}
|
||||
|
||||
beginTest ("Ranges::set() - setting a range inside another range");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
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 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
expectRange (ranges.get (2), { 49, 94 });
|
||||
expectRange (ranges.get (3), { 94, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::split()");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ranges.set ({ 0, 48 }, ops);
|
||||
ranges.set ({ 48, 127 }, ops);
|
||||
|
||||
ops.clear();
|
||||
ranges.split (47, ops);
|
||||
|
||||
expectEquals ((int64) ops.size(), (int64) 1, "");
|
||||
const auto op0 = std::get_if<Ranges::Ops::Split> (&ops[0]);
|
||||
expect (op0 != nullptr);
|
||||
|
||||
if (op0 != nullptr)
|
||||
expectEquals ((int) op0->index, 0, "The 0th element should be split");
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 3, "");
|
||||
expectRange (ranges.get (0), { 0, 47 });
|
||||
expectRange (ranges.get (1), { 47, 48 });
|
||||
expectRange (ranges.get (2), { 48, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::split() - splitting has no effect when no range begins before and ends after the location");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ranges.set ({ 0, 48 }, ops);
|
||||
ranges.set ({ 48, 127 }, ops);
|
||||
|
||||
ops.clear();
|
||||
ranges.split (48, ops);
|
||||
expectEquals ((int64) ops.size(), (int64) 0, "");
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 2, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::insert() - basics");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ranges.insert ({ -3, 14 }, ops);
|
||||
expectRange (ranges.get (0), { -3, 14 });
|
||||
|
||||
ranges.insert ({ 7, 20 }, ops);
|
||||
expectRange (ranges.get (0), { -3, 7 });
|
||||
expectRange (ranges.get (1), { 7, 20 });
|
||||
expectRange (ranges.get (2), { 20, 27 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::insert() - inserting shifts all following ranges");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ranges.insert ({ 10, 11 }, ops);
|
||||
ranges.insert ({ 0, 1 }, ops);
|
||||
|
||||
expectRange (ranges.get (0), { 0, 1 });
|
||||
expectRange (ranges.get (1), { 11, 12 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::insert() - inserting an empty range should be a no-op");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ops.clear();
|
||||
ranges.insert ({ 0, 0 }, ops);
|
||||
expect (ranges.isEmpty());
|
||||
expect (ops.empty());
|
||||
}
|
||||
|
||||
const auto getTestRanges = [&ops]
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
ranges.set ({ 0, 48 }, ops);
|
||||
ranges.set ({ 48, 49 }, ops);
|
||||
ranges.set ({ 55, 94 }, ops);
|
||||
ranges.set ({ 94, 127 }, ops);
|
||||
|
||||
return ranges;
|
||||
};
|
||||
|
||||
beginTest ("Ranges::eraseFrom() - erasing beyond all ranges has no effect");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
ranges.eraseFrom (ranges.get (ranges.size() - 1).getEnd() + 5, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 4, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
expectRange (ranges.get (2), { 55, 94 });
|
||||
expectRange (ranges.get (3), { 94, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::eraseFrom() - erasing modifies the range that encloses the starting index");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
ranges.eraseFrom (122, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 4, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
expectRange (ranges.get (2), { 55, 94 });
|
||||
expectRange (ranges.get (3), { 94, 122 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::eraseFrom() - ranges starting after the erased index are deleted entirely");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
ranges.eraseFrom (60, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 3, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
expectRange (ranges.get (2), { 55, 60 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::eraseFrom() - erasing from a location outside any ranges will still drop subsequent ranges");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
ranges.eraseFrom (51, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 2, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::erase() - erasing a zero length range has no effect");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.erase ({ 30, 30 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 4, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
expectRange (ranges.get (2), { 55, 94 });
|
||||
expectRange (ranges.get (3), { 94, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::erase() - erasing inside a range splits that range");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.erase ({ 30, 31 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 5, "");
|
||||
expectRange (ranges.get (0), { 0, 30 });
|
||||
expectRange (ranges.get (1), { 31, 48 });
|
||||
expectRange (ranges.get (2), { 48, 49 });
|
||||
expectRange (ranges.get (3), { 55, 94 });
|
||||
expectRange (ranges.get (4), { 94, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::erase() - erasing a range that completely overlaps existing ranges erases those");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.erase ({ 30, 70 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 3, "");
|
||||
expectRange (ranges.get (0), { 0, 30 });
|
||||
expectRange (ranges.get (1), { 70, 94 });
|
||||
expectRange (ranges.get (2), { 94, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::erase() - erasing a range that no previous ranges covered has no effect");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.erase ({ 51, 53 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 4, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
expectRange (ranges.get (2), { 55, 94 });
|
||||
expectRange (ranges.get (3), { 94, 127 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::erase() - erasing over a range beyond all limits clears all ranges");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.erase ({ -1000, 1000 }, ops);
|
||||
|
||||
expect (ranges.isEmpty());
|
||||
}
|
||||
|
||||
beginTest ("Ranges::drop() - dropping a range shifts all following ranges downward");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.drop ({ 48, 49 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 3, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 54, 93 });
|
||||
expectRange (ranges.get (2), { 93, 126 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::drop() - dropping a range shifts all following ranges downward even if no range covered the dropped range previously");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.drop ({ 51, 53 }, ops);
|
||||
|
||||
expectEquals ((int64) ranges.size(), (int64) 4, "");
|
||||
expectRange (ranges.get (0), { 0, 48 });
|
||||
expectRange (ranges.get (1), { 48, 49 });
|
||||
expectRange (ranges.get (2), { 53, 92 });
|
||||
expectRange (ranges.get (3), { 92, 125 });
|
||||
}
|
||||
|
||||
beginTest ("Ranges::drop() - dropping a range covering all other ranges empties the collection");
|
||||
{
|
||||
auto ranges = getTestRanges();
|
||||
|
||||
ranges.drop ({ -1000, 1000 }, ops);
|
||||
expect (ranges.isEmpty());
|
||||
}
|
||||
|
||||
beginTest ("Ranges::covers()");
|
||||
{
|
||||
Ranges ranges;
|
||||
|
||||
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 }));
|
||||
expect (ranges.covers ({ 10, 30 }));
|
||||
expect (ranges.covers ({ 30, 48 }));
|
||||
expect (ranges.covers ({ 30, 49 }));
|
||||
expect (ranges.covers ({ 55, 150 }));
|
||||
expect (ranges.covers ({ 60, 145 }));
|
||||
|
||||
expect (! ranges.covers ({ -1, 10 }));
|
||||
expect (! ranges.covers ({ 1, 50 }));
|
||||
expect (! ranges.covers ({ 50, 140 }));
|
||||
expect (! ranges.covers ({ 149, 151 }));
|
||||
|
||||
expect (ranges.covers ({ 10, 10 }));
|
||||
expect (! ranges.covers ({ 151, 151 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class RangedValuesTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
RangedValuesTests() : UnitTest ("RangedValues", UnitTestCategories::containers) {}
|
||||
|
||||
template <typename ItemType>
|
||||
void expectRangedValuesItem (ItemType item, Range<int64> range, char value)
|
||||
{
|
||||
{
|
||||
String failureMessage { "range " };
|
||||
failureMessage << item.range << " did not equal expected range " << range;
|
||||
expect (item.range == range, failureMessage);
|
||||
}
|
||||
|
||||
{
|
||||
String failureMessage { "value '" };
|
||||
failureMessage << item.value << "' in range " << range << " did not equal expected value '" << value << "'";
|
||||
expect (item.value == value, failureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
auto random = getRandom();
|
||||
Ranges::Operations ops;
|
||||
|
||||
const auto createRangedValuesObject = [&]
|
||||
{
|
||||
RangedValues<char> rangedValues;
|
||||
|
||||
rangedValues.set ({ 0, 10 }, 'a', ops);
|
||||
rangedValues.set ({ 11, 20 }, 'b', ops);
|
||||
rangedValues.set ({ 23, 30 }, 'c', ops);
|
||||
|
||||
return rangedValues;
|
||||
};
|
||||
|
||||
beginTest ("RangedValues::set() with distinct value overlapping other ranges");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
rangedValues.set ({ 5, 15 }, 'd', ops);
|
||||
|
||||
expect (! rangedValues.isEmpty());
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 5 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 5, 15 }, 'd');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 15, 20 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 23, 30 }, 'c');
|
||||
|
||||
rangedValues.set ({ 19, 24 }, 'e', ops);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 15, 19 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 19, 24 }, 'e');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 24, 30 }, 'c');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::set() with distinct value in corner cases");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
rangedValues.set ({ -1, 0 }, 'd', ops);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { -1, 0 }, 'd');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 0, 10 }, 'a');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::set() with same value with merging disallowed");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
rangedValues.set ({ 5, 15 }, 'b', ops, MergeEqualItemsNo{});
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 5 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 5, 15 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 15, 20 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 23, 30 }, 'c');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::set() with same value with merging allowed");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
rangedValues.set ({ 5, 15 }, 'b', ops, MergeEqualItemsYes{});
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 5 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 5, 20 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 23, 30 }, 'c');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::set() - setting an empty Range should be a no-op");
|
||||
{
|
||||
RangedValues<char> rangedValues;
|
||||
|
||||
ops.clear();
|
||||
rangedValues.set ({ 0, 0 }, 'a', ops);
|
||||
expect (rangedValues.isEmpty());
|
||||
expect (ops.empty());
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::set() - setting a range inside another range");
|
||||
{
|
||||
RangedValues<char> rangedValues;
|
||||
|
||||
rangedValues.set ({ 0, 48 }, 'a', ops);
|
||||
rangedValues.set ({ 48, 127 }, 'b', ops);
|
||||
rangedValues.set ({ 49, 94 }, 'c', ops);
|
||||
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 4, "");
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 48 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 48, 49 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 49, 94 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 94, 127 }, 'b');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::getIntersectionsWith()");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
{
|
||||
const auto intersections = rangedValues.getIntersectionsWith ({ 5, 43 });
|
||||
|
||||
expectRangedValuesItem (intersections.getItem (0), { 5, 10 }, 'a');
|
||||
expectRangedValuesItem (intersections.getItem (1), { 11, 20 }, 'b');
|
||||
expectRangedValuesItem (intersections.getItem (2), { 23, 30 }, 'c');
|
||||
}
|
||||
|
||||
{
|
||||
const auto intersections = rangedValues.getIntersectionsWith ({ -10, 3 });
|
||||
|
||||
expectRangedValuesItem (intersections.getItem (0), { 0, 3 }, 'a');
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::insert() fuzzing - insert always increases the total covered range");
|
||||
{
|
||||
for (auto i = 0; i != 100; ++i)
|
||||
{
|
||||
auto rangedValuesNotMerged = createRangedValuesObject();
|
||||
auto rangedValuesMerged = createRangedValuesObject();
|
||||
|
||||
const auto totalLengthBeforeInsert = getCumulativeRangeLengths (rangedValuesNotMerged);
|
||||
|
||||
const auto beginInsertionAt = (int64) random.nextInt (100) - 50;
|
||||
const auto numElemsToInsert = (int64) random.nextInt (1000);
|
||||
|
||||
rangedValuesNotMerged.insert ({ Range<int64>::withStartAndLength (beginInsertionAt, numElemsToInsert) },
|
||||
'a' + (char) random.nextInt (25),
|
||||
ops,
|
||||
MergeEqualItemsNo{});
|
||||
|
||||
expectEquals (getCumulativeRangeLengths (rangedValuesNotMerged) - totalLengthBeforeInsert, numElemsToInsert);
|
||||
|
||||
rangedValuesMerged.insert ({ Range<int64>::withStartAndLength (beginInsertionAt, numElemsToInsert) },
|
||||
'a' + (char) random.nextInt (25),
|
||||
ops,
|
||||
MergeEqualItemsYes{});
|
||||
|
||||
expectEquals (getCumulativeRangeLengths (rangedValuesMerged) - totalLengthBeforeInsert, numElemsToInsert);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::insert() with distinct value inside another range");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 3);
|
||||
|
||||
rangedValues.insert ({ 2, 4 }, 'd', ops);
|
||||
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 5);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 2 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 2, 4 }, 'd');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 4, 12 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 13, 22 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 25, 32 }, 'c');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::insert() with same value inside another range");
|
||||
{
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 3);
|
||||
|
||||
rangedValues.insert ({ 2, 4 }, 'a', ops, MergeEqualItemsYes{});
|
||||
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 3);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 12 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 13, 22 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 25, 32 }, 'c');
|
||||
}
|
||||
{
|
||||
auto rangedValues = createRangedValuesObject();
|
||||
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 3);
|
||||
|
||||
rangedValues.insert ({ 2, 4 }, 'a', ops, MergeEqualItemsNo{});
|
||||
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 5);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 2 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 2, 4 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 4, 12 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 13, 22 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 25, 32 }, 'c');
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::insert() - inserting an empty Range should be a no-op");
|
||||
{
|
||||
{
|
||||
RangedValues<char> emptyRangedValues;
|
||||
|
||||
ops.clear();
|
||||
emptyRangedValues.insert ({ 0, 0 }, 'a', ops);
|
||||
expect (emptyRangedValues.isEmpty());
|
||||
expect (ops.empty());
|
||||
}
|
||||
|
||||
{
|
||||
RangedValues<char> rangedValues;
|
||||
rangedValues.set ({ 0, 10 }, 'a', ops);
|
||||
|
||||
ops.clear();
|
||||
rangedValues.insert ({ 0, 0 }, 'a', ops);
|
||||
expect (ops.empty());
|
||||
expectEquals ((int64) rangedValues.size(), (int64) 1);
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
|
||||
}
|
||||
}
|
||||
|
||||
const auto createRangedValuesObjectForErase = [&]
|
||||
{
|
||||
RangedValues<char> rangedValues;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
beginTest ("RangedValues::erase() - erase will not shift subsequent ranges downward");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObjectForErase();
|
||||
|
||||
rangedValues.erase ({ 15, 16 }, ops);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 11, 15 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 16, 20 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 23, 30 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 35, 45 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (5), { 45, 60 }, 'd');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::eraseUpTo() - erasing before all ranges has no effect");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObjectForErase();
|
||||
|
||||
rangedValues.eraseUpTo (rangedValues.getRanges().get (0).getStart(), ops);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 11, 20 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 23, 30 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 35, 45 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 45, 60 }, 'd');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::eraseUpTo() - erasing values up to, not including 15");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObjectForErase();
|
||||
|
||||
rangedValues.eraseUpTo (15, ops);
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 15, 20 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 23, 30 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 35, 45 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 45, 60 }, 'd');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::eraseUpTo() - erasing up to the end of all ranges clears the container");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObjectForErase();
|
||||
const auto ranges = rangedValues.getRanges();
|
||||
|
||||
rangedValues.eraseUpTo (ranges.get (ranges.size() - 1).getEnd(), ops);
|
||||
|
||||
expect (rangedValues.isEmpty());
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::drop() - drop shifts ranges downward on the right side");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObjectForErase();
|
||||
|
||||
rangedValues.drop ({ 15, 16 }, ops, MergeEqualItemsNo{});
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 11, 15 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 15, 19 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 22, 29 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 34, 44 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (5), { 44, 59 }, 'd');
|
||||
}
|
||||
|
||||
beginTest ("RangedValues::drop() - drop can be used to merge equal values");
|
||||
{
|
||||
auto rangedValues = createRangedValuesObjectForErase();
|
||||
|
||||
rangedValues.drop ({ 15, 16 }, ops, MergeEqualItemsYes{});
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 11, 19 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 22, 29 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 34, 44 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 44, 59 }, 'd');
|
||||
}
|
||||
|
||||
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', ops, MergeEqualItemsNo{});
|
||||
|
||||
rangedValues.drop ({ 15, 16 }, ops, MergeEqualItemsYes{});
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (0), { 0, 10 }, 'a');
|
||||
|
||||
// These two items are not merged, even though they form a contiguous range, because
|
||||
// they were disjoint before the drop and they don't touch each other at the drop
|
||||
// seam of 15.
|
||||
expectRangedValuesItem (rangedValues.getItem (1), { 11, 19 }, 'b');
|
||||
expectRangedValuesItem (rangedValues.getItem (2), { 19, 29 }, 'b');
|
||||
|
||||
expectRangedValuesItem (rangedValues.getItem (3), { 34, 44 }, 'c');
|
||||
expectRangedValuesItem (rangedValues.getItem (4), { 44, 59 }, 'd');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class IntersectingRangedValuesTests : public RangesTestsBase
|
||||
{
|
||||
public:
|
||||
IntersectingRangedValuesTests() : RangesTestsBase ("IntersectingRangedValues", UnitTestCategories::containers) {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
Ranges::Operations ops;
|
||||
|
||||
beginTest ("IntersectingRangedValuesTests - iterating over multiple RangedValues");
|
||||
{
|
||||
RangedValues<int> rv1;
|
||||
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, 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, ops);
|
||||
rv3.set ({ 15, 19}, -2, ops);
|
||||
rv3.set ({ 22, 36}, -3, ops);
|
||||
|
||||
int iteration = 0;
|
||||
|
||||
for (const auto [range, v1, v2, v3] : makeIntersectingRangedValues (&rv1, &rv2, &rv3))
|
||||
{
|
||||
if (iteration == 0)
|
||||
{
|
||||
expectRange (range, Range<int64> { 3, 4 });
|
||||
expect (v1 == 1 && v2 == 7 && v3 == -1);
|
||||
}
|
||||
|
||||
if (iteration == 1)
|
||||
{
|
||||
expectRange (range, Range<int64> { 4, 6 });
|
||||
expect (v1 == 1 && v2 == 11 && v3 == -1);
|
||||
}
|
||||
|
||||
if (iteration == 2)
|
||||
{
|
||||
expectRange (range, Range<int64> { 6, 8 });
|
||||
expect (v1 == 1 && v2 == 13 && v3 == -1);
|
||||
}
|
||||
|
||||
if (iteration == 3)
|
||||
{
|
||||
expectRange (range, Range<int64> { 9, 10 });
|
||||
expect (v1 == 2 && v2 == 13 && v3 == -1);
|
||||
}
|
||||
|
||||
if (iteration == 4)
|
||||
{
|
||||
expectRange (range, Range<int64> { 15, 16 });
|
||||
expect (v1 == 2 && v2 == 13 && v3 == -2);
|
||||
}
|
||||
|
||||
if (iteration == 5)
|
||||
{
|
||||
expectRange (range, Range<int64> { 30, 36 });
|
||||
expect (v1 == 3 && v2 == 17 && v3 == -3);
|
||||
}
|
||||
|
||||
++iteration;
|
||||
}
|
||||
|
||||
expectEquals (iteration, 6);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static RangesTests rangesTests;
|
||||
static RangedValuesTests rangedValuesTests;
|
||||
static IntersectingRangedValuesTests intersectingRangedValuesTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce::detail
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
class ShapedText::Impl
|
||||
{
|
||||
public:
|
||||
Impl (String textIn, Options optionsIn)
|
||||
: options { std::move (optionsIn) },
|
||||
text { std::move (textIn) }
|
||||
{
|
||||
}
|
||||
|
||||
void draw (const Graphics& g, AffineTransform transform) const
|
||||
{
|
||||
drawJustifiedText (justifiedText, g, transform);
|
||||
}
|
||||
|
||||
float getHeight() const
|
||||
{
|
||||
return justifiedText.getHeight();
|
||||
}
|
||||
|
||||
int64 getNumGlyphs() const
|
||||
{
|
||||
return simpleShapedText.getNumGlyphs();
|
||||
}
|
||||
|
||||
const detail::RangedValues<LineMetrics>& getLineMetricsForGlyphRange() const
|
||||
{
|
||||
return justifiedText.getLineMetricsForGlyphRange();
|
||||
}
|
||||
|
||||
const detail::Ranges& getLineTextRanges() const
|
||||
{
|
||||
return simpleShapedText.getLineTextRanges();
|
||||
}
|
||||
|
||||
auto& getText() const
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
auto getTextRange (int64 glyphIndex) const
|
||||
{
|
||||
return simpleShapedText.getTextRange (glyphIndex);
|
||||
}
|
||||
|
||||
auto isLtr (int64 glyphIndex) const
|
||||
{
|
||||
return simpleShapedText.isLtr (glyphIndex);
|
||||
}
|
||||
|
||||
int64 getTextIndexForCaret (Point<float> p) const
|
||||
{
|
||||
const auto getGlyph = [&] (int64 i)
|
||||
{
|
||||
return simpleShapedText.getGlyphs()[(size_t) i];
|
||||
};
|
||||
|
||||
if (getNumGlyphs() == 0)
|
||||
return 0;
|
||||
|
||||
const auto glyphOnTheRight = justifiedText.getGlyphIndexToTheRightOf (p);
|
||||
|
||||
if (glyphOnTheRight >= getNumGlyphs())
|
||||
{
|
||||
const auto glyphOnTheLeft = glyphOnTheRight - 1;
|
||||
const auto ltr = simpleShapedText.getGlyphLookup().find (getGlyph (glyphOnTheLeft).cluster)->value.ltr;
|
||||
|
||||
if (ltr)
|
||||
return simpleShapedText.getTextIndexAfterGlyph (glyphOnTheLeft);
|
||||
|
||||
return simpleShapedText.getGlyphs()[(size_t) glyphOnTheLeft].cluster;
|
||||
}
|
||||
|
||||
const auto ltr = simpleShapedText.getGlyphLookup().find (getGlyph (glyphOnTheRight).cluster)->value.ltr;
|
||||
|
||||
if (ltr)
|
||||
return simpleShapedText.getGlyphs()[(size_t) glyphOnTheRight].cluster;
|
||||
|
||||
return simpleShapedText.getTextIndexAfterGlyph (glyphOnTheRight);
|
||||
}
|
||||
|
||||
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const
|
||||
{
|
||||
simpleShapedText.getGlyphRanges (textRange, outRanges);
|
||||
}
|
||||
|
||||
RectangleList<float> getGlyphsBounds (Range<int64> glyphRange) const
|
||||
{
|
||||
return justifiedText.getGlyphsBounds (glyphRange);
|
||||
}
|
||||
|
||||
auto getGlyphAnchor (int64 index) const
|
||||
{
|
||||
return justifiedText.getGlyphAnchor (index);
|
||||
}
|
||||
|
||||
Span<const float> getMinimumRequiredWidthForLines() const
|
||||
{
|
||||
return justifiedText.getMinimumRequiredWidthForLines();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
auto& getSimpleShapedText() const { return simpleShapedText; }
|
||||
|
||||
auto& getJustifiedText() const { return justifiedText; }
|
||||
|
||||
private:
|
||||
ShapedTextOptions options;
|
||||
String text;
|
||||
SimpleShapedText simpleShapedText { &text, options };
|
||||
JustifiedText justifiedText { &simpleShapedText, options };
|
||||
};
|
||||
|
||||
ShapedText::ShapedText() : ShapedText ("", {})
|
||||
{
|
||||
}
|
||||
|
||||
ShapedText::ShapedText (String text) : ShapedText (std::move (text), {})
|
||||
{
|
||||
}
|
||||
|
||||
ShapedText::ShapedText (String text, Options options)
|
||||
{
|
||||
impl = std::make_shared<Impl> (std::move (text), std::move (options));
|
||||
}
|
||||
|
||||
void ShapedText::draw (const Graphics& g, AffineTransform transform) const
|
||||
{
|
||||
impl->draw (g, transform);
|
||||
}
|
||||
|
||||
float ShapedText::getHeight() const
|
||||
{
|
||||
return impl->getHeight();
|
||||
}
|
||||
|
||||
int64 ShapedText::getNumGlyphs() const
|
||||
{
|
||||
return impl->getNumGlyphs();
|
||||
}
|
||||
|
||||
const detail::RangedValues<LineMetrics>& ShapedText::getLineMetricsForGlyphRange() const
|
||||
{
|
||||
return impl->getLineMetricsForGlyphRange();
|
||||
}
|
||||
|
||||
const detail::Ranges& ShapedText::getLineTextRanges() const
|
||||
{
|
||||
return impl->getLineTextRanges();
|
||||
}
|
||||
|
||||
const String& ShapedText::getText() const
|
||||
{
|
||||
return impl->getText();
|
||||
}
|
||||
|
||||
Range<int64> ShapedText::getTextRange (int64 glyphIndex) const
|
||||
{
|
||||
return impl->getTextRange (glyphIndex);
|
||||
}
|
||||
|
||||
bool ShapedText::isLtr (int64 glyphIndex) const
|
||||
{
|
||||
return impl->isLtr (glyphIndex);
|
||||
}
|
||||
|
||||
int64 ShapedText::getTextIndexForCaret (Point<float> p) const
|
||||
{
|
||||
return impl->getTextIndexForCaret (p);
|
||||
}
|
||||
|
||||
void ShapedText::getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const
|
||||
{
|
||||
return impl->getGlyphRanges (textRange, outRanges);
|
||||
}
|
||||
|
||||
RectangleList<float> ShapedText::getGlyphsBounds (Range<int64> glyphRange) const
|
||||
{
|
||||
return impl->getGlyphsBounds (glyphRange);
|
||||
}
|
||||
|
||||
GlyphAnchorResult ShapedText::getGlyphAnchor (int64 index) const
|
||||
{
|
||||
return impl->getGlyphAnchor (index);
|
||||
}
|
||||
|
||||
Span<const float> ShapedText::getMinimumRequiredWidthForLines() const
|
||||
{
|
||||
return impl->getMinimumRequiredWidthForLines();
|
||||
}
|
||||
|
||||
const JustifiedText& ShapedText::getJustifiedText() const { return impl->getJustifiedText(); }
|
||||
|
||||
const SimpleShapedText& ShapedText::getSimpleShapedText() const { return impl->getSimpleShapedText(); }
|
||||
|
||||
} // namespace juce::detail
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
/* Class that can visually shape a Unicode string provided a list of Fonts corresponding to
|
||||
sub-ranges of the string.
|
||||
*/
|
||||
class JUCE_API ShapedText
|
||||
{
|
||||
public:
|
||||
using Options = ShapedTextOptions;
|
||||
|
||||
ShapedText();
|
||||
|
||||
explicit ShapedText (String text);
|
||||
|
||||
ShapedText (String text, Options options);
|
||||
|
||||
/* Returns the text which was used to construct this object. */
|
||||
const String& getText() const;
|
||||
|
||||
Span<const ShapedGlyph> getGlyphs() const;
|
||||
|
||||
/* Returns the text's codepoint range, to which the glyph under the provided index belongs.
|
||||
|
||||
This range will have a length of at least one, and potentially more than one if ligatures
|
||||
are enabled.
|
||||
*/
|
||||
Range<int64> getTextRange (int64 glyphIndex) const;
|
||||
|
||||
bool isLtr (int64 glyphIndex) const;
|
||||
|
||||
int64 getTextIndexForCaret (Point<float> p) const;
|
||||
|
||||
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const;
|
||||
|
||||
RectangleList<float> getGlyphsBounds (Range<int64> glyphRange) const;
|
||||
|
||||
/* @see JustifiedText::getGlyphAnchor() */
|
||||
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
|
||||
line based on the ShapedTextOptions::withTrailingWhitespacesShouldFit() value.
|
||||
|
||||
This value isn't affected by the Justification parameter, it just reports the amount of
|
||||
width that would be required to avoid truncation.
|
||||
*/
|
||||
Span<const float> getMinimumRequiredWidthForLines() const;
|
||||
|
||||
/* @see JustifiedText::accessTogetherWith */
|
||||
template <typename Callable, typename... RangedValues>
|
||||
void accessTogetherWith (Callable&& callback, RangedValues&&... rangedValues) const
|
||||
{
|
||||
getJustifiedText().accessTogetherWith (std::forward<Callable> (callback),
|
||||
std::forward<RangedValues> (rangedValues)...);
|
||||
}
|
||||
|
||||
/* Draws the text. */
|
||||
void draw (const Graphics& g, AffineTransform transform) const;
|
||||
|
||||
/* @see JustifiedText::getHeight
|
||||
*/
|
||||
float getHeight() const;
|
||||
|
||||
int64 getNumGlyphs() const;
|
||||
|
||||
const detail::RangedValues<LineMetrics>& getLineMetricsForGlyphRange() const;
|
||||
|
||||
const detail::Ranges& getLineTextRanges() const;
|
||||
|
||||
/* @internal */
|
||||
const JustifiedText& getJustifiedText() const;
|
||||
|
||||
/* @internal */
|
||||
const SimpleShapedText& getSimpleShapedText() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::shared_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
} // namespace juce::detail
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
/** Types of text direction. This may also be applied to characters. */
|
||||
enum class TextDirection
|
||||
{
|
||||
ltr, // This text reads left to right.
|
||||
rtl // This text reads right to left.
|
||||
};
|
||||
|
||||
class ShapedTextOptions
|
||||
{
|
||||
private:
|
||||
auto tie() const
|
||||
{
|
||||
return std::tie (justification,
|
||||
readingDir,
|
||||
maxWidth,
|
||||
alignmentWidth,
|
||||
height,
|
||||
fontsForRange,
|
||||
language,
|
||||
firstLineIndent,
|
||||
leading,
|
||||
additiveLineSpacing,
|
||||
baselineAtZero,
|
||||
allowBreakingInsideWord,
|
||||
trailingWhitespacesShouldFit,
|
||||
maxNumLines,
|
||||
ellipsis);
|
||||
}
|
||||
|
||||
public:
|
||||
bool operator== (const ShapedTextOptions& other) const { return tie() == other.tie(); }
|
||||
bool operator!= (const ShapedTextOptions& other) const { return tie() != other.tie(); }
|
||||
|
||||
//==============================================================================
|
||||
[[nodiscard]] ShapedTextOptions withJustification (Justification x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::justification, x);
|
||||
}
|
||||
|
||||
/* This option will use soft wrapping for lines that are longer than the specified value,
|
||||
and it will also align each line to this width, using the Justification provided in
|
||||
withJustification.
|
||||
|
||||
The alignment width can be overriden using withAlignmentWidth, but currently we only need
|
||||
to do this for the TextEditor.
|
||||
*/
|
||||
[[nodiscard]] ShapedTextOptions withMaxWidth (float x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::maxWidth, x);
|
||||
}
|
||||
|
||||
/* With this option each line will be aligned only if it's shorter or equal to the alignment
|
||||
width. Otherwise, the line's x anchor will be 0.0f. This is in contrast to using
|
||||
withMaxWidth only, which will modify the x anchor of RTL lines that are too long, to ensure
|
||||
that it's the logical end of the text that falls outside the visible bounds.
|
||||
|
||||
The alignment width is also a distinct value from the value used for soft wrapping which is
|
||||
specified using withMaxWidth.
|
||||
|
||||
This option is specifically meant to support an existing TextEditor behaviour, where text
|
||||
can be aligned even when word wrapping is off. You probably don't need to use this function,
|
||||
unless you want to reproduce the particular behaviour seen in the TextEditor, and should
|
||||
only use withMaxWidth, if alignment is required.
|
||||
|
||||
With this option off, text is either not aligned, or aligned to the width specified using
|
||||
withMaxWidth.
|
||||
|
||||
When this option is in use, it overrides the width specified in withMaxWidth for alignment
|
||||
purposes, but not for line wrapping purposes.
|
||||
|
||||
It also accommodates the fact that the TextEditor has a scrolling feature and text never
|
||||
becomes unreachable, even if the lines are longer than the viewport's width.
|
||||
*/
|
||||
[[nodiscard]] ShapedTextOptions withAlignmentWidth (float x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::alignmentWidth, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withHeight (float x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::height, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withFont (Font x) const
|
||||
{
|
||||
RangedValues<Font> fonts;
|
||||
detail::Ranges::Operations ops;
|
||||
fonts.set ({ 0, std::numeric_limits<int64>::max() }, x, ops);
|
||||
|
||||
return withMember (*this, &ShapedTextOptions::fontsForRange, std::move (fonts));
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withFonts (const detail::RangedValues<Font>& x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::fontsForRange, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withLanguage (StringRef x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::language, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withFirstLineIndent (float x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::firstLineIndent, x);
|
||||
}
|
||||
|
||||
/* This controls the space between lines using a proportional value, with a default of 1.0,
|
||||
meaning single line spacing i.e. the descender of the current line + ascender of the next
|
||||
line. This value is multiplied by the leading provided here.
|
||||
*/
|
||||
[[nodiscard]] ShapedTextOptions withLeading (float x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::leading, x);
|
||||
}
|
||||
|
||||
/* This controls the space between lines using an additive absolute value, with a default of 0.0.
|
||||
This value is added to the spacing between each two lines.
|
||||
*/
|
||||
[[nodiscard]] ShapedTextOptions withAdditiveLineSpacing (float x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::additiveLineSpacing, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withBaselineAtZero (bool x = true) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::baselineAtZero, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withTrailingWhitespacesShouldFit (bool x = true) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::trailingWhitespacesShouldFit, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withMaxNumLines (int64 x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::maxNumLines, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withEllipsis (String x = String::charToString ((juce_wchar) 0x2026)) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::ellipsis, std::move (x));
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withReadingDirection (std::optional<TextDirection> x) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::readingDir, x);
|
||||
}
|
||||
|
||||
[[nodiscard]] ShapedTextOptions withAllowBreakingInsideWord (bool x = true) const
|
||||
{
|
||||
return withMember (*this, &ShapedTextOptions::allowBreakingInsideWord, x);
|
||||
}
|
||||
|
||||
const auto& getReadingDirection() const { return readingDir; }
|
||||
const auto& getJustification() const { return justification; }
|
||||
const auto& getMaxWidth() const { return maxWidth; }
|
||||
const auto& getAlignmentWidth() const { return alignmentWidth; }
|
||||
const auto& getHeight() const { return height; }
|
||||
const auto& getFontsForRange() const { return fontsForRange; }
|
||||
const auto& getLanguage() const { return language; }
|
||||
const auto& getFirstLineIndent() const { return firstLineIndent; }
|
||||
const auto& getLeading() const { return leading; }
|
||||
const auto& getAdditiveLineSpacing() const { return additiveLineSpacing; }
|
||||
const auto& isBaselineAtZero() const { return baselineAtZero; }
|
||||
const auto& getTrailingWhitespacesShouldFit() const { return trailingWhitespacesShouldFit; }
|
||||
const auto& getMaxNumLines() const { return maxNumLines; }
|
||||
const auto& getEllipsis() const { return ellipsis; }
|
||||
const auto& getAllowBreakingInsideWord() const { return allowBreakingInsideWord; }
|
||||
|
||||
private:
|
||||
Justification justification { Justification::topLeft };
|
||||
std::optional<TextDirection> readingDir;
|
||||
std::optional<float> maxWidth;
|
||||
std::optional<float> alignmentWidth;
|
||||
std::optional<float> height;
|
||||
|
||||
detail::RangedValues<Font> fontsForRange = std::invoke ([&]
|
||||
{
|
||||
detail::RangedValues<Font> result;
|
||||
detail::Ranges::Operations ops;
|
||||
result.set ({ 0, std::numeric_limits<int64>::max() }, FontOptions { 15.0f }, ops);
|
||||
return result;
|
||||
});
|
||||
|
||||
String language = SystemStats::getDisplayLanguage();
|
||||
float firstLineIndent = 0.0f;
|
||||
float leading = 1.0f;
|
||||
float additiveLineSpacing = 0.0f;
|
||||
bool baselineAtZero = false;
|
||||
bool allowBreakingInsideWord = false;
|
||||
bool trailingWhitespacesShouldFit = true;
|
||||
int64 maxNumLines = std::numeric_limits<int64>::max();
|
||||
String ellipsis;
|
||||
};
|
||||
|
||||
struct ShapedGlyph
|
||||
{
|
||||
ShapedGlyph (uint32_t glyphIdIn,
|
||||
int64 clusterIn,
|
||||
bool unsafeToBreakIn,
|
||||
bool whitespaceIn,
|
||||
bool newlineIn,
|
||||
int8_t distanceFromLigatureIn,
|
||||
Point<float> advanceIn,
|
||||
Point<float> offsetIn)
|
||||
: advance (advanceIn),
|
||||
offset (offsetIn),
|
||||
cluster (clusterIn),
|
||||
glyphId (glyphIdIn),
|
||||
unsafeToBreak (unsafeToBreakIn),
|
||||
whitespace (whitespaceIn),
|
||||
newline (newlineIn),
|
||||
distanceFromLigature (distanceFromLigatureIn) {}
|
||||
|
||||
bool isUnsafeToBreak() const { return unsafeToBreak; }
|
||||
bool isWhitespace() const { return whitespace; }
|
||||
bool isNewline() const { return newline; }
|
||||
|
||||
bool isNonLigature() const { return distanceFromLigature == 0; }
|
||||
bool isLigature() const { return distanceFromLigature < 0; }
|
||||
bool isPlaceholderForLigature() const { return distanceFromLigature > 0; }
|
||||
|
||||
int8_t getDistanceFromLigature() const { return distanceFromLigature; }
|
||||
int8_t getNumTrailingLigaturePlaceholders() const { return -distanceFromLigature; }
|
||||
|
||||
Point<float> advance;
|
||||
Point<float> offset;
|
||||
int64 cluster;
|
||||
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;
|
||||
int8_t distanceFromLigature;
|
||||
};
|
||||
|
||||
struct GlyphLookupEntry
|
||||
{
|
||||
Range<int64> glyphRange;
|
||||
bool ltr = true;
|
||||
};
|
||||
|
||||
class SimpleShapedText
|
||||
{
|
||||
public:
|
||||
/* Shapes and lays out the first contiguous sequence of ranges specified in the fonts
|
||||
parameter.
|
||||
*/
|
||||
SimpleShapedText (const String* data,
|
||||
const ShapedTextOptions& options);
|
||||
|
||||
const auto& getLineNumbersForGlyphRanges() const { return lineNumbersForGlyphRanges; }
|
||||
|
||||
const auto& getLineTextRanges() const { return lineTextRanges; }
|
||||
|
||||
const auto& getResolvedFonts() const { return resolvedFonts; }
|
||||
|
||||
Range<int64> getTextRange (int64 glyphIndex) const;
|
||||
|
||||
/* Returns true if the specified glyph is inside to an LTR run.
|
||||
*/
|
||||
bool isLtr (int64 glyphIndex) const;
|
||||
|
||||
/* This function may fail to return an out range, even if the provided textRange falls inside
|
||||
the string range used for the creation of the ShapedText object.
|
||||
|
||||
This is because the shaping process could fail due to insufficient glyph resolution to the
|
||||
point, where it will produce zero glyphs for the provided string.
|
||||
*/
|
||||
void getGlyphRanges (Range<int64> textRange, std::vector<Range<int64>>& outRanges) const;
|
||||
|
||||
/* Returns the input codepoint index that follows the glyph in a logical sense. So for LTR text
|
||||
this is the cluster number of the glyph to the right. For RTL text it's the one on the left.
|
||||
|
||||
If there is no subsequent glyph, the returned number is the first Unicode codepoint index
|
||||
that isn't covered by the cluster to which the selected glyph belongs, so for the glyph 'o'
|
||||
in "hello" this would be 5, given there are no ligatures in use.
|
||||
*/
|
||||
int64 getTextIndexAfterGlyph (int64 glyphIndex) const;
|
||||
|
||||
int64 getNumLines() const { return (int64) lineNumbersForGlyphRanges.getRanges().size(); }
|
||||
int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); }
|
||||
|
||||
juce_wchar getCodepoint (int64 glyphIndex) const;
|
||||
|
||||
Span<const ShapedGlyph> getGlyphs (Range<int64> glyphRange) const;
|
||||
|
||||
Span<const ShapedGlyph> getGlyphs() const;
|
||||
|
||||
const auto& getGlyphLookup() const { return glyphLookup; }
|
||||
|
||||
private:
|
||||
void shape (const String& data,
|
||||
const ShapedTextOptions& options);
|
||||
|
||||
const String& string;
|
||||
std::vector<ShapedGlyph> glyphsInVisualOrder;
|
||||
detail::RangedValues<int64> lineNumbersForGlyphRanges;
|
||||
detail::Ranges lineTextRanges;
|
||||
detail::RangedValues<Font> resolvedFonts;
|
||||
detail::RangedValues<GlyphLookupEntry> glyphLookup;
|
||||
|
||||
JUCE_LEAK_DETECTOR (SimpleShapedText)
|
||||
};
|
||||
|
||||
} // namespace juce::detail
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::detail
|
||||
{
|
||||
|
||||
struct UnicodeHelpers
|
||||
{
|
||||
UnicodeHelpers() = delete;
|
||||
|
||||
static std::vector<int> getLineBreaks (const String& data);
|
||||
};
|
||||
|
||||
} // namespace juce::detail
|
||||
Reference in New Issue
Block a user