CFAttributedString ranges must be given in terms of 16-bit word offsets, rather than codepoints.v6.1.6
| @@ -54,6 +54,24 @@ namespace | |||||
| } | } | ||||
| } | } | ||||
| inline bool areInvariantsMaintained (const String& text, const Array<AttributedString::Attribute>& 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<int> splitAttributeRanges (Array<AttributedString::Attribute>& atts, Range<int> newRange) | Range<int> splitAttributeRanges (Array<AttributedString::Attribute>& atts, Range<int> newRange) | ||||
| { | { | ||||
| newRange = newRange.getIntersectionWith ({ 0, getLength (atts) }); | newRange = newRange.getIntersectionWith ({ 0, getLength (atts) }); | ||||
| @@ -151,30 +169,35 @@ void AttributedString::setText (const String& newText) | |||||
| truncate (attributes, newLength); | truncate (attributes, newLength); | ||||
| text = newText; | text = newText; | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::append (const String& textToAppend) | void AttributedString::append (const String& textToAppend) | ||||
| { | { | ||||
| text += textToAppend; | text += textToAppend; | ||||
| appendRange (attributes, textToAppend.length(), nullptr, nullptr); | appendRange (attributes, textToAppend.length(), nullptr, nullptr); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::append (const String& textToAppend, const Font& font) | void AttributedString::append (const String& textToAppend, const Font& font) | ||||
| { | { | ||||
| text += textToAppend; | text += textToAppend; | ||||
| appendRange (attributes, textToAppend.length(), &font, nullptr); | appendRange (attributes, textToAppend.length(), &font, nullptr); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::append (const String& textToAppend, Colour colour) | void AttributedString::append (const String& textToAppend, Colour colour) | ||||
| { | { | ||||
| text += textToAppend; | text += textToAppend; | ||||
| appendRange (attributes, textToAppend.length(), nullptr, &colour); | appendRange (attributes, textToAppend.length(), nullptr, &colour); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::append (const String& textToAppend, const Font& font, Colour colour) | void AttributedString::append (const String& textToAppend, const Font& font, Colour colour) | ||||
| { | { | ||||
| text += textToAppend; | text += textToAppend; | ||||
| appendRange (attributes, textToAppend.length(), &font, &colour); | appendRange (attributes, textToAppend.length(), &font, &colour); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::append (const AttributedString& other) | void AttributedString::append (const AttributedString& other) | ||||
| @@ -188,6 +211,7 @@ void AttributedString::append (const AttributedString& other) | |||||
| attributes.getReference (i).range += originalLength; | attributes.getReference (i).range += originalLength; | ||||
| mergeAdjacentRanges (attributes); | mergeAdjacentRanges (attributes); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::clear() | void AttributedString::clear() | ||||
| @@ -219,21 +243,25 @@ void AttributedString::setLineSpacing (const float newLineSpacing) noexcept | |||||
| void AttributedString::setColour (Range<int> range, Colour colour) | void AttributedString::setColour (Range<int> range, Colour colour) | ||||
| { | { | ||||
| applyFontAndColour (attributes, range, nullptr, &colour); | applyFontAndColour (attributes, range, nullptr, &colour); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::setFont (Range<int> range, const Font& font) | void AttributedString::setFont (Range<int> range, const Font& font) | ||||
| { | { | ||||
| applyFontAndColour (attributes, range, &font, nullptr); | applyFontAndColour (attributes, range, &font, nullptr); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::setColour (Colour colour) | void AttributedString::setColour (Colour colour) | ||||
| { | { | ||||
| setColour ({ 0, getLength (attributes) }, colour); | setColour ({ 0, getLength (attributes) }, colour); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::setFont (const Font& font) | void AttributedString::setFont (const Font& font) | ||||
| { | { | ||||
| setFont ({ 0, getLength (attributes) }, font); | setFont ({ 0, getLength (attributes) }, font); | ||||
| jassert (areInvariantsMaintained (text, attributes)); | |||||
| } | } | ||||
| void AttributedString::draw (Graphics& g, const Rectangle<float>& area) const | void AttributedString::draw (Graphics& g, const Rectangle<float>& area) const | ||||
| @@ -34,6 +34,11 @@ namespace juce | |||||
| An attributed string lets you create a string with varied fonts, colours, word-wrapping, | An attributed string lets you create a string with varied fonts, colours, word-wrapping, | ||||
| layout, etc., and draw it using AttributedString::draw(). | 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 | @see TextLayout | ||||
| @tags{Graphics} | @tags{Graphics} | ||||
| @@ -258,20 +258,24 @@ namespace CoreTextTypeLayout | |||||
| auto attribString = CFAttributedStringCreateMutable (kCFAllocatorDefault, 0); | auto attribString = CFAttributedStringCreateMutable (kCFAllocatorDefault, 0); | ||||
| CFUniquePtr<CFStringRef> cfText (text.getText().toCFString()); | CFUniquePtr<CFStringRef> cfText (text.getText().toCFString()); | ||||
| CFAttributedStringReplaceString (attribString, CFRangeMake (0, 0), cfText.get()); | 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; | 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)) | if (auto ctFontRef = getOrCreateFont (attr.font)) | ||||
| { | { | ||||
| @@ -271,12 +271,22 @@ namespace DirectWriteTypeLayout | |||||
| format.SetWordWrapping (wrapType); | 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; | 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); | auto familyName = FontStyleHelpers::getConcreteFamilyName (attr.font); | ||||
| @@ -367,18 +377,28 @@ namespace DirectWriteTypeLayout | |||||
| hr = dwTextFormat->SetTrimming (&trimming, trimmingSign); | 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) | if (FAILED (hr) || textLayout == nullptr) | ||||
| return false; | return false; | ||||
| auto numAttributes = text.getNumAttributes(); | |||||
| const auto numAttributes = text.getNumAttributes(); | |||||
| auto rangePointer = beginPtr; | |||||
| for (int i = 0; i < numAttributes; ++i) | 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; | return true; | ||||
| } | } | ||||