From e27fb35996c7b17e86f9961c93343ca861f47dba Mon Sep 17 00:00:00 2001 From: reuk Date: Fri, 11 Feb 2022 17:40:23 +0000 Subject: [PATCH] Fonts: Adjust attribute ranges correctly when rendering AttributedStrings CFAttributedString ranges must be given in terms of 16-bit word offsets, rather than codepoints. --- .../fonts/juce_AttributedString.cpp | 28 ++++++++++++++ .../fonts/juce_AttributedString.h | 5 +++ .../juce_graphics/native/juce_mac_Fonts.mm | 18 +++++---- .../juce_win32_DirectWriteTypeLayout.cpp | 38 ++++++++++++++----- 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/modules/juce_graphics/fonts/juce_AttributedString.cpp b/modules/juce_graphics/fonts/juce_AttributedString.cpp index 2a2ef4b7c8..ce09e29a4f 100644 --- a/modules/juce_graphics/fonts/juce_AttributedString.cpp +++ b/modules/juce_graphics/fonts/juce_AttributedString.cpp @@ -54,6 +54,24 @@ namespace } } + inline bool areInvariantsMaintained (const String& text, const Array& atts) + { + if (atts.isEmpty()) + return true; + + if (atts.getFirst().range.getStart() != 0) + return false; + + if (atts.getLast().range.getEnd() != text.length()) + return false; + + for (auto it = std::next (atts.begin()); it != atts.end(); ++it) + if (it->range.getStart() != std::prev (it)->range.getEnd()) + return false; + + return true; + } + Range splitAttributeRanges (Array& atts, Range newRange) { newRange = newRange.getIntersectionWith ({ 0, getLength (atts) }); @@ -151,30 +169,35 @@ void AttributedString::setText (const String& newText) truncate (attributes, newLength); text = newText; + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::append (const String& textToAppend) { text += textToAppend; appendRange (attributes, textToAppend.length(), nullptr, nullptr); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::append (const String& textToAppend, const Font& font) { text += textToAppend; appendRange (attributes, textToAppend.length(), &font, nullptr); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::append (const String& textToAppend, Colour colour) { text += textToAppend; appendRange (attributes, textToAppend.length(), nullptr, &colour); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::append (const String& textToAppend, const Font& font, Colour colour) { text += textToAppend; appendRange (attributes, textToAppend.length(), &font, &colour); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::append (const AttributedString& other) @@ -188,6 +211,7 @@ void AttributedString::append (const AttributedString& other) attributes.getReference (i).range += originalLength; mergeAdjacentRanges (attributes); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::clear() @@ -219,21 +243,25 @@ void AttributedString::setLineSpacing (const float newLineSpacing) noexcept void AttributedString::setColour (Range range, Colour colour) { applyFontAndColour (attributes, range, nullptr, &colour); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::setFont (Range range, const Font& font) { applyFontAndColour (attributes, range, &font, nullptr); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::setColour (Colour colour) { setColour ({ 0, getLength (attributes) }, colour); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::setFont (const Font& font) { setFont ({ 0, getLength (attributes) }, font); + jassert (areInvariantsMaintained (text, attributes)); } void AttributedString::draw (Graphics& g, const Rectangle& area) const diff --git a/modules/juce_graphics/fonts/juce_AttributedString.h b/modules/juce_graphics/fonts/juce_AttributedString.h index 9721939a06..5f7fffdfbf 100644 --- a/modules/juce_graphics/fonts/juce_AttributedString.h +++ b/modules/juce_graphics/fonts/juce_AttributedString.h @@ -34,6 +34,11 @@ namespace juce An attributed string lets you create a string with varied fonts, colours, word-wrapping, layout, etc., and draw it using AttributedString::draw(). + Invariants: + - Every character in the string is a member of exactly one attribute. + - Attributes are sorted such that the range-end of attribute 'i' is equal to the + range-begin of attribute 'i + 1'. + @see TextLayout @tags{Graphics} diff --git a/modules/juce_graphics/native/juce_mac_Fonts.mm b/modules/juce_graphics/native/juce_mac_Fonts.mm index dc1de4e025..83ba29472a 100644 --- a/modules/juce_graphics/native/juce_mac_Fonts.mm +++ b/modules/juce_graphics/native/juce_mac_Fonts.mm @@ -258,20 +258,24 @@ namespace CoreTextTypeLayout auto attribString = CFAttributedStringCreateMutable (kCFAllocatorDefault, 0); CFUniquePtr cfText (text.getText().toCFString()); + CFAttributedStringReplaceString (attribString, CFRangeMake (0, 0), cfText.get()); - auto numCharacterAttributes = text.getNumAttributes(); - auto attribStringLen = CFAttributedStringGetLength (attribString); + const auto numCharacterAttributes = text.getNumAttributes(); + const auto attribStringLen = CFAttributedStringGetLength (attribString); + const auto beginPtr = text.getText().toUTF16(); + auto currentPosition = beginPtr; - for (int i = 0; i < numCharacterAttributes; ++i) + for (int i = 0; i < numCharacterAttributes; currentPosition += text.getAttribute (i).range.getLength(), ++i) { - auto& attr = text.getAttribute (i); - auto rangeStart = attr.range.getStart(); + const auto& attr = text.getAttribute (i); + const auto wordBegin = currentPosition.getAddress() - beginPtr.getAddress(); - if (rangeStart >= attribStringLen) + if (attribStringLen <= wordBegin) continue; - auto range = CFRangeMake (rangeStart, jmin (attr.range.getEnd(), (int) attribStringLen) - rangeStart); + const auto wordEnd = jmin (attribStringLen, (currentPosition + attr.range.getLength()).getAddress() - beginPtr.getAddress()); + const auto range = CFRangeMake (wordBegin, wordEnd - wordBegin); if (auto ctFontRef = getOrCreateFont (attr.font)) { diff --git a/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp b/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp index a877e559b6..28931e9f33 100644 --- a/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp +++ b/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp @@ -271,12 +271,22 @@ namespace DirectWriteTypeLayout format.SetWordWrapping (wrapType); } - void addAttributedRange (const AttributedString::Attribute& attr, IDWriteTextLayout& textLayout, - const int textLen, ID2D1RenderTarget& renderTarget, IDWriteFontCollection& fontCollection) + void addAttributedRange (const AttributedString::Attribute& attr, + IDWriteTextLayout& textLayout, + CharPointer_UTF16 begin, + CharPointer_UTF16 textPointer, + const UINT32 textLen, + ID2D1RenderTarget& renderTarget, + IDWriteFontCollection& fontCollection) { DWRITE_TEXT_RANGE range; - range.startPosition = (UINT32) attr.range.getStart(); - range.length = (UINT32) jmin (attr.range.getLength(), textLen - attr.range.getStart()); + range.startPosition = (UINT32) (textPointer.getAddress() - begin.getAddress()); + + if (textLen <= range.startPosition) + return; + + const auto wordEnd = jmin (textLen, (UINT32) ((textPointer + attr.range.getLength()).getAddress() - begin.getAddress())); + range.length = wordEnd - range.startPosition; { auto familyName = FontStyleHelpers::getConcreteFamilyName (attr.font); @@ -367,18 +377,28 @@ namespace DirectWriteTypeLayout hr = dwTextFormat->SetTrimming (&trimming, trimmingSign); } - auto textLen = text.getText().length(); + const auto beginPtr = text.getText().toUTF16(); + const auto textLen = (UINT32) (beginPtr.findTerminatingNull().getAddress() - beginPtr.getAddress()); - hr = directWriteFactory.CreateTextLayout (text.getText().toWideCharPointer(), (UINT32) textLen, dwTextFormat, - maxWidth, maxHeight, textLayout.resetAndGetPointerAddress()); + hr = directWriteFactory.CreateTextLayout (beginPtr.getAddress(), + textLen, + dwTextFormat, + maxWidth, + maxHeight, + textLayout.resetAndGetPointerAddress()); if (FAILED (hr) || textLayout == nullptr) return false; - auto numAttributes = text.getNumAttributes(); + const auto numAttributes = text.getNumAttributes(); + auto rangePointer = beginPtr; for (int i = 0; i < numAttributes; ++i) - addAttributedRange (text.getAttribute (i), *textLayout, textLen, renderTarget, fontCollection); + { + const auto attribute = text.getAttribute (i); + addAttributedRange (attribute, *textLayout, beginPtr, rangePointer, textLen, renderTarget, fontCollection); + rangePointer += attribute.range.getLength(); + } return true; }