diff --git a/modules/juce_graphics/detail/juce_JustifiedText.cpp b/modules/juce_graphics/detail/juce_JustifiedText.cpp index 5d7571e04e..eb91cf6568 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.cpp +++ b/modules/juce_graphics/detail/juce_JustifiedText.cpp @@ -449,6 +449,69 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions realign.extraWhitespaceAdvance); } +int64 JustifiedText::getGlyphIndexAt (Point p) const +{ + auto lineIt = linesMetrics.begin(); + float lineTop = 0.0f; + + while (lineIt != linesMetrics.end()) + { + const auto nextLineTop = lineIt->value.nextLineTop; + + if (lineTop <= p.getY() && p.getY() < nextLineTop) + break; + + lineTop = nextLineTop; + ++lineIt; + } + + if (lineIt == linesMetrics.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.newline + || (glyphIndex - lineIt->range.getStart() == (int64) glyphsInLine.size() - 1 && glyph.whitespace)) + { + break; + } + + ++glyphIndex; + glyphX += glyph.advance.getX(); + } + + return glyphIndex; +} + +Point JustifiedText::getGlyphAnchor (int64 index) const +{ + jassert (index >= 0); + + if (linesMetrics.isEmpty()) + return {}; + + const auto lineItem = linesMetrics.getItemWithEnclosingRange (index).value_or (linesMetrics.back()); + const auto indexInLine = index - lineItem.range.getStart(); + + auto anchor = lineItem.value.anchor; + + for (auto [i, glyph] : enumerate (shapedText.getGlyphs (lineItem.range), int64{})) + { + if (i == indexInLine) + return anchor + glyph.offset; + + anchor += glyph.advance; + } + + return anchor; +} + 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 57a6c9acaa..7afd5a9e02 100644 --- a/modules/juce_graphics/detail/juce_JustifiedText.h +++ b/modules/juce_graphics/detail/juce_JustifiedText.h @@ -153,6 +153,14 @@ public: */ auto& getMinimumRequiredWidthForLines() const { return minimumRequiredWidthsForLine; } + int64 getGlyphIndexAt (Point 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. + */ + Point getGlyphAnchor (int64 index) 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. diff --git a/modules/juce_graphics/detail/juce_ShapedText.cpp b/modules/juce_graphics/detail/juce_ShapedText.cpp index 627275fac8..7279c6b471 100644 --- a/modules/juce_graphics/detail/juce_ShapedText.cpp +++ b/modules/juce_graphics/detail/juce_ShapedText.cpp @@ -54,6 +54,11 @@ public: return justifiedText.getHeight(); } + int64 getNumGlyphs() const + { + return simpleShapedText.getNumGlyphs(); + } + auto& getText() const { return text; @@ -64,6 +69,21 @@ public: return simpleShapedText.getTextRange (glyphIndex); } + int64 getGlyphIndexAt (Point p) const + { + return justifiedText.getGlyphIndexAt (p); + } + + auto getGlyphRanges (Range textRange) const + { + return simpleShapedText.getGlyphRanges (textRange); + } + + auto getGlyphAnchor (int64 index) const + { + return justifiedText.getGlyphAnchor (index); + } + Span getMinimumRequiredWidthForLines() const { return justifiedText.getMinimumRequiredWidthForLines(); @@ -104,6 +124,11 @@ float ShapedText::getHeight() const return impl->getHeight(); } +int64 ShapedText::getNumGlyphs() const +{ + return impl->getNumGlyphs(); +} + const String& ShapedText::getText() const { return impl->getText(); @@ -114,6 +139,21 @@ Range ShapedText::getTextRange (int64 glyphIndex) const return impl->getTextRange (glyphIndex); } +int64 ShapedText::getGlyphIndexAt (Point p) const +{ + return impl->getGlyphIndexAt (p); +} + +std::vector> ShapedText::getGlyphRanges (Range textRange) const +{ + return impl->getGlyphRanges (textRange); +} + +Point ShapedText::getGlyphAnchor (int64 index) const +{ + return impl->getGlyphAnchor (index); +} + Span ShapedText::getMinimumRequiredWidthForLines() const { return impl->getMinimumRequiredWidthForLines(); diff --git a/modules/juce_graphics/detail/juce_ShapedText.h b/modules/juce_graphics/detail/juce_ShapedText.h index 5f964d163b..e5024677aa 100644 --- a/modules/juce_graphics/detail/juce_ShapedText.h +++ b/modules/juce_graphics/detail/juce_ShapedText.h @@ -59,6 +59,13 @@ public: */ Range getTextRange (int64 glyphIndex) const; + int64 getGlyphIndexAt (Point p) const; + + std::vector> getGlyphRanges (Range textRange) const; + + /* @see JustifiedText::getGlyphAnchor() */ + Point 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. @@ -79,8 +86,12 @@ public: /* Draws the text. */ void draw (const Graphics& g, AffineTransform transform) const; + /* @see JustifiedText::getHeight + */ float getHeight() const; + int64 getNumGlyphs() 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 c68a5f986c..d7f7614a58 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.cpp +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.cpp @@ -343,6 +343,16 @@ static std::vector lowLevelShape (const String& string, && font.getTypefacePtr()->getGlyphBounds (font.getMetricsKind(), (int) glyphId).isEmpty() && xAdvance > 0; + const auto newline = std::invoke ([&controlChars, &shapingInfos = infos, j] + { + const auto it = controlChars.find ((size_t) shapingInfos[j].cluster); + + if (it == controlChars.end()) + return false; + + return it->second == ControlCharacter::cr || it->second == ControlCharacter::lf; + }); + // Tracking is only applied at the beginning of a new cluster to avoid inserting it before // diacritic marks. const auto appliedTracking = std::exchange (lastCluster, infos[j].cluster) != infos[j].cluster @@ -354,6 +364,7 @@ static std::vector lowLevelShape (const String& string, (int64) infos[j].cluster + range.getStart(), (infos[j].mask & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) != 0, whitespace, + newline, Point { HbScale::hbToJuce (xAdvance) + appliedTracking, -HbScale::hbToJuce (positions[j].y_advance) }, Point { HbScale::hbToJuce (positions[j].x_offset), -HbScale::hbToJuce (positions[j].y_offset) }, }); @@ -1286,6 +1297,75 @@ 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. + + NB: lower_bound: equal or greater + upper_bound: greater + lessThanOrEqual: less than or equal +*/ +template +auto lessThanOrEqual (It begin, It end, Value v, Callback extractValue) +{ + auto it = std::lower_bound (begin, + end, + v, + [&extractValue] (auto& elem, auto& value) + { + return extractValue (elem) < value; + }); + + if (it == end || it == begin || extractValue (*it) == v) + return it; + + --it; + + return it; +} + +std::vector> SimpleShapedText::getGlyphRanges (Range textRange) const +{ + Ranges glyphRanges; + + for (const auto is : glyphLookup.getIntersectionsWith (textRange)) + { + const auto textSubRange = is.range; + const auto glyphsSubRange = is.value.glyphRange; + const auto& subRangeLookup = is.value; + const auto glyphs = getGlyphs (glyphsSubRange); + + const auto getGlyphSubRange = [&] (auto begin, auto end) + { + auto startIt = lessThanOrEqual (begin, + end, + textSubRange.getStart(), + [] (auto& elem) -> auto& { return elem.cluster; }); + + auto endIt = std::lower_bound (begin, + end, + textSubRange.getEnd(), + [] (auto& elem, auto& value) { return elem.cluster < value; }); + + return Range { (int64) std::distance (begin, startIt), std::distance (begin, endIt) }; + }; + + if (subRangeLookup.ltr) + { + glyphRanges.set (getGlyphSubRange (glyphs.begin(), glyphs.end()) + glyphsSubRange.getStart()); + } + else + { + const auto reverseRange = getGlyphSubRange (std::reverse_iterator { glyphs.end() }, + std::reverse_iterator { glyphs.begin() }); + + glyphRanges.set ({ glyphsSubRange.getEnd() - reverseRange.getEnd(), + glyphsSubRange.getEnd() - reverseRange.getStart() }); + } + } + + return glyphRanges.getRanges(); +} + #if JUCE_UNIT_TESTS struct SimpleShapedTextTests : public UnitTest diff --git a/modules/juce_graphics/detail/juce_SimpleShapedText.h b/modules/juce_graphics/detail/juce_SimpleShapedText.h index 5c0b7efd33..c08283c105 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.h +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.h @@ -175,6 +175,7 @@ struct ShapedGlyph int64 cluster; bool unsafeToBreak; bool whitespace; + bool newline; Point advance; Point offset; }; @@ -203,6 +204,8 @@ public: Range getTextRange (int64 glyphIndex) const; + std::vector> getGlyphRanges (Range textRange) const; + int64 getNumLines() const { return (int64) lineNumbers.getRanges().size(); } int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); }