1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

ShapedText: Add bidirectional lookup functions relating positions, input indices and glyph indices

This commit is contained in:
attila 2025-03-05 17:49:10 +01:00 committed by Attila Szarvas
parent 6cf697bd6d
commit d72df5faa3
6 changed files with 205 additions and 0 deletions

View file

@ -449,6 +449,69 @@ JustifiedText::JustifiedText (const SimpleShapedText* t, const ShapedTextOptions
realign.extraWhitespaceAdvance);
}
int64 JustifiedText::getGlyphIndexAt (Point<float> 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<float> 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())

View file

@ -153,6 +153,14 @@ public:
*/
auto& getMinimumRequiredWidthForLines() const { return minimumRequiredWidthsForLine; }
int64 getGlyphIndexAt (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.
*/
Point<float> 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.

View file

@ -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<float> p) const
{
return justifiedText.getGlyphIndexAt (p);
}
auto getGlyphRanges (Range<int64> textRange) const
{
return simpleShapedText.getGlyphRanges (textRange);
}
auto getGlyphAnchor (int64 index) const
{
return justifiedText.getGlyphAnchor (index);
}
Span<const float> 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<int64> ShapedText::getTextRange (int64 glyphIndex) const
return impl->getTextRange (glyphIndex);
}
int64 ShapedText::getGlyphIndexAt (Point<float> p) const
{
return impl->getGlyphIndexAt (p);
}
std::vector<Range<int64>> ShapedText::getGlyphRanges (Range<int64> textRange) const
{
return impl->getGlyphRanges (textRange);
}
Point<float> ShapedText::getGlyphAnchor (int64 index) const
{
return impl->getGlyphAnchor (index);
}
Span<const float> ShapedText::getMinimumRequiredWidthForLines() const
{
return impl->getMinimumRequiredWidthForLines();

View file

@ -59,6 +59,13 @@ public:
*/
Range<int64> getTextRange (int64 glyphIndex) const;
int64 getGlyphIndexAt (Point<float> p) const;
std::vector<Range<int64>> getGlyphRanges (Range<int64> textRange) const;
/* @see JustifiedText::getGlyphAnchor() */
Point<float> 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;

View file

@ -343,6 +343,16 @@ static std::vector<ShapedGlyph> 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<ShapedGlyph> lowLevelShape (const String& string,
(int64) infos[j].cluster + range.getStart(),
(infos[j].mask & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) != 0,
whitespace,
newline,
Point<float> { HbScale::hbToJuce (xAdvance) + appliedTracking, -HbScale::hbToJuce (positions[j].y_advance) },
Point<float> { HbScale::hbToJuce (positions[j].x_offset), -HbScale::hbToJuce (positions[j].y_offset) },
});
@ -1286,6 +1297,75 @@ Range<int64> SimpleShapedText::getTextRange (int64 glyphIndex) const
return Range<int64>::withStartAndLength (cluster, std::max ((int64) 1, nextAdjacentCluster - cluster));
}
/* Returns the last element that is smaller than value or equal to it. Returns begin of no such
element is found.
NB: lower_bound: equal or greater
upper_bound: greater
lessThanOrEqual: less than or equal
*/
template <typename It, typename Value, typename Callback>
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<Range<int64>> SimpleShapedText::getGlyphRanges (Range<int64> 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> { (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

View file

@ -175,6 +175,7 @@ struct ShapedGlyph
int64 cluster;
bool unsafeToBreak;
bool whitespace;
bool newline;
Point<float> advance;
Point<float> offset;
};
@ -203,6 +204,8 @@ public:
Range<int64> getTextRange (int64 glyphIndex) const;
std::vector<Range<int64>> getGlyphRanges (Range<int64> textRange) const;
int64 getNumLines() const { return (int64) lineNumbers.getRanges().size(); }
int64 getNumGlyphs() const { return (int64) glyphsInVisualOrder.size(); }