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; | ||||
} | } | ||||