From 3a0135ffb7f2bfe05a99f9b4daf89beba7e4d7eb Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 15 Oct 2025 13:23:28 +0100 Subject: [PATCH] SimpleShapedText: Fix detection of control characters Newlines get removed in the sanitised string, so we need to take extra steps to keep track of their positions. Co-authored-by: Aga Janowicz --- .../detail/juce_SimpleShapedText.cpp | 106 +++++++++--------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/modules/juce_graphics/detail/juce_SimpleShapedText.cpp b/modules/juce_graphics/detail/juce_SimpleShapedText.cpp index becf4c0e1c..456570704c 100644 --- a/modules/juce_graphics/detail/juce_SimpleShapedText.cpp +++ b/modules/juce_graphics/detail/juce_SimpleShapedText.cpp @@ -153,18 +153,6 @@ static std::optional findControlCharacter (CharPtr it, CharPtr return {}; } -static auto findControlCharacters (Span string) -{ - std::map result; - size_t index = 0; - - for (auto it = string.begin(); it != string.end(); ++it, ++index) - if (const auto cc = findControlCharacter (it, string.end())) - result[index] = *cc; - - return result; -} - static constexpr hb_feature_t hbFeature (FontFeatureSetting setting) { return { setting.tag.getTag(), @@ -223,37 +211,63 @@ static auto incrementCharPtr (CharPtr b, CharPtr e, int64 steps) return b; } -static std::vector sanitiseString (const String& stringIn, Range lineRange) +class SanitisedString { - std::vector result; - - const auto end = stringIn.end(); - const auto beginOfRange = incrementCharPtr (stringIn.begin(), end, lineRange.getStart()); - const auto endOfRange = incrementCharPtr (beginOfRange, end, lineRange.getLength()); - - result.reserve (beginOfRange.lengthUpTo (endOfRange)); - - for (auto it = beginOfRange; it != endOfRange; ++it) +public: + static SanitisedString sanitise (const String& stringIn, Range lineRange) { - result.push_back (std::invoke ([&] + SanitisedString result; + + const auto end = stringIn.end(); + const auto beginOfRange = incrementCharPtr (stringIn.begin(), end, lineRange.getStart()); + const auto endOfRange = incrementCharPtr (beginOfRange, end, lineRange.getLength()); + + result.characters.reserve (beginOfRange.lengthUpTo (endOfRange)); + + for (auto it = beginOfRange; it != endOfRange; ++it) { const auto cc = findControlCharacter (it, end); - if (! cc.has_value()) - return *it; + if (cc == ControlCharacter::cr || cc == ControlCharacter::lf) + result.newlineIndices.push_back (result.characters.size()); - constexpr juce_wchar wordJoiner = 0x2060; - constexpr juce_wchar nonBreakingSpace = 0x00a0; + result.characters.push_back (std::invoke ([&] + { + if (! cc.has_value()) + return *it; - return cc == ControlCharacter::crFollowedByLf ? wordJoiner : nonBreakingSpace; - })); + constexpr juce_wchar wordJoiner = 0x2060; + constexpr juce_wchar nonBreakingSpace = 0x00a0; + + return cc == ControlCharacter::crFollowedByLf ? wordJoiner : nonBreakingSpace; + })); + } + + return result; } - return result; -} + bool isNewline (size_t index) const + { + return std::binary_search (newlineIndices.begin(), newlineIndices.end(), index); + } + + const juce_wchar* data() const + { + return characters.data(); + } + + size_t size() const + { + return characters.size(); + } + +private: + std::vector characters; + std::vector newlineIndices; +}; /* Returns glyphs in logical order as that favours wrapping. */ -static std::vector lowLevelShape (Span string, +static std::vector lowLevelShape (const SanitisedString& string, Range range, const Font& font, TextScript script, @@ -278,7 +292,6 @@ static std::vector lowLevelShape (Span string, 0); const Span shapedSpan { string.data() + range.getStart(), (size_t) range.getLength() }; - const auto controlChars = findControlCharacters (shapedSpan); for (const auto pair : enumerate (shapedSpan, size_t{})) hb_buffer_add (buffer.get(), static_cast (pair.value), (unsigned int) pair.index); @@ -305,13 +318,13 @@ static std::vector lowLevelShape (Span string, hb_shape (nativeFont.get(), buffer.get(), features.data(), (unsigned int) features.size()); - const auto [infos, positions] = [&buffer] + const auto [infos, positions] = std::invoke ([&buffer] { unsigned int count{}; return std::make_pair (Span { hb_buffer_get_glyph_infos (buffer.get(), &count), (size_t) count }, Span { hb_buffer_get_glyph_positions (buffer.get(), &count), (size_t) count }); - }(); + }); jassert (infos.size() == positions.size()); @@ -369,16 +382,7 @@ static std::vector lowLevelShape (Span string, && font.getTypefacePtr()->getGlyphBounds (font.getMetricsKind(), (int) glyphId).isEmpty() && xAdvanceBase > 0; - const auto newline = std::invoke ([&controlChars, &shapingInfos = infos, visualIndex] - { - const auto it = controlChars.find ((size_t) shapingInfos[visualIndex].cluster); - - if (it == controlChars.end()) - return false; - - return it->second == ControlCharacter::cr || it->second == ControlCharacter::lf; - }); - + const auto newline = string.isNewline (infos[visualIndex].cluster + (size_t) range.getStart()); const auto cluster = (int64) infos[visualIndex].cluster + range.getStart(); const auto numLigaturePlaceholders = std::max ((int64) 0, @@ -393,7 +397,7 @@ static std::vector lowLevelShape (Span string, const auto advanceMultiplier = numLigaturePlaceholders == 0 ? 1.0f : 1.0f / (float) (numLigaturePlaceholders + 1); - Point advance { xAdvanceBase * advanceMultiplier + appliedTracking, yAdvanceBase * advanceMultiplier }; + const Point advance { xAdvanceBase * advanceMultiplier + appliedTracking, yAdvanceBase * advanceMultiplier }; const auto ligatureClusterNumber = cluster + (ltr ? 0 : numLigaturePlaceholders); @@ -405,8 +409,8 @@ static std::vector lowLevelShape (Span string, newline, numLigaturePlaceholders == 0 ? (int8_t) 0 : (int8_t) -numLigaturePlaceholders , advance, - Point { HbScale::hbToJuce (positions[visualIndex].x_offset), - -HbScale::hbToJuce (positions[visualIndex].y_offset) }, + Point { HbScale::hbToJuce (positions[visualIndex].x_offset), + -HbScale::hbToJuce (positions[visualIndex].y_offset) }, }); for (int l = 0; l < numLigaturePlaceholders; ++l) @@ -421,7 +425,7 @@ static std::vector lowLevelShape (Span string, newline, (int8_t) (l + 1), advance, - Point{}, + {}, }); } } @@ -884,7 +888,7 @@ static auto rangedValuesWithOffset (detail::RangedValues rv, int64 offset = 0 struct Shaper { Shaper (const String& stringIn, Range lineRange, const ShapedTextOptions& options) - : string (sanitiseString (stringIn, lineRange)) + : string (SanitisedString::sanitise (stringIn, lineRange)) { const auto analysis = Unicode::performAnalysis (stringIn.substring ((int) lineRange.getStart(), (int) lineRange.getEnd())); @@ -1043,7 +1047,7 @@ struct Shaper return result; } - std::vector string; + SanitisedString string; std::vector visualOrder; RangedValues shaperRuns; std::vector softBreakBeforePoints;