diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index ab35e407fa..86fac94e50 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -2,6 +2,38 @@ # Version 8.0.0 +## Change + +As part of the Unicode upgrades the vertical alignment logic of TextLayout has +been altered. Lines containing text written in multiple different fonts will +now have their baselines aligned. Additionally, using the +Justification::verticallyCentred or Justification::bottom flags may now result +in the text being positioned slightly differently. + +**Possible Issues** + +User interfaces using TextLayout with texts drawn using multiple fonts will now +have their look changed. + +**Workaround** + +There is no workaround. + +**Rationale** + +The old implementation had incosistent vertical alignment behaviour. Depending +on what exact fonts the first line of text happened to use, the bottom alignment +would sometimes produce unnecessary padding on the bottom. With certain text and +Font combinations the text would be drawn beyond the bottom boundary even though +there was free space above the text. + +The same amount of incorrect vertical offset, that was calculated for bottom +alignment, was also present when using centred, it just wasn't as apparent. + +Not having the baselines aligned between different fonts resulted in generally +displeasing visuals. + + ## Change The virtual functions LowLevelGraphicsContext::drawGlyph() and drawTextLayout() diff --git a/modules/juce_graphics/fonts/juce_TextLayout.cpp b/modules/juce_graphics/fonts/juce_TextLayout.cpp index 8cedae2e3a..30442a7a98 100644 --- a/modules/juce_graphics/fonts/juce_TextLayout.cpp +++ b/modules/juce_graphics/fonts/juce_TextLayout.cpp @@ -35,11 +35,6 @@ namespace juce { -static String substring (const String& text, Range range) -{ - return text.substring (range.getStart(), range.getEnd()); -} - TextLayout::Glyph::Glyph (int glyph, Point anch, float w) noexcept : glyphCode (glyph), anchor (anch), width (w) { @@ -325,279 +320,141 @@ void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& te } //============================================================================== -namespace TextLayoutHelpers +template +static auto castTo (const Range& r) { - struct Token - { - Token (const String& t, const Font& f, Colour c, bool whitespace) - : text (t), font (f), colour (c), - area (font.getStringWidthFloat (t), f.getHeight()), - isWhitespace (whitespace), - isNewLine (t.containsChar ('\n') || t.containsChar ('\r')) - {} - - const String text; - const Font font; - const Colour colour; - Rectangle area; - int line; - float lineHeight; - const bool isWhitespace, isNewLine; - - Token& operator= (const Token&) = delete; - }; - - struct TokenList - { - TokenList() noexcept {} - - void createLayout (const AttributedString& text, TextLayout& layout) - { - layout.ensureStorageAllocated (totalLines); - - addTextRuns (text); - layoutRuns (layout.getWidth(), text.getLineSpacing(), text.getWordWrap()); - - int charPosition = 0; - int lineStartPosition = 0; - int runStartPosition = 0; - - std::unique_ptr currentLine; - std::unique_ptr currentRun; - - bool needToSetLineOrigin = true; - - for (int i = 0; i < tokens.size(); ++i) - { - auto& t = *tokens.getUnchecked (i); - - Array newGlyphs; - Array xOffsets; - t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets); - - if (currentRun == nullptr) currentRun = std::make_unique(); - if (currentLine == nullptr) currentLine = std::make_unique(); - - const auto numGlyphs = newGlyphs.size(); - charPosition += numGlyphs; - - if (numGlyphs > 0 - && (! (t.isWhitespace || t.isNewLine) || needToSetLineOrigin)) - { - currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size()); - auto tokenOrigin = t.area.getPosition().translated (0, t.font.getAscent()); - - if (needToSetLineOrigin) - { - needToSetLineOrigin = false; - currentLine->lineOrigin = tokenOrigin; - } - - auto glyphOffset = tokenOrigin - currentLine->lineOrigin; - - for (int j = 0; j < newGlyphs.size(); ++j) - { - auto x = xOffsets.getUnchecked (j); - currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked (j), - glyphOffset.translated (x, 0), - xOffsets.getUnchecked (j + 1) - x)); - } - } - - if (auto* nextToken = tokens[i + 1]) - { - if (t.font != nextToken->font || t.colour != nextToken->colour) - { - addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition); - runStartPosition = charPosition; - } - - if (t.line != nextToken->line) - { - if (currentRun == nullptr) - currentRun = std::make_unique(); - - addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition); - currentLine->stringRange = { lineStartPosition, charPosition }; - - if (! needToSetLineOrigin) - layout.addLine (std::move (currentLine)); - - runStartPosition = charPosition; - lineStartPosition = charPosition; - needToSetLineOrigin = true; - } - } - else - { - addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition); - currentLine->stringRange = { lineStartPosition, charPosition }; - - if (! needToSetLineOrigin) - layout.addLine (std::move (currentLine)); - - needToSetLineOrigin = true; - } - } - - if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0) - { - auto totalW = layout.getWidth(); - bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0; - - for (auto& line : layout) - { - auto dx = totalW - line.getLineBoundsX().getLength(); - - if (isCentred) - dx /= 2.0f; - - line.lineOrigin.x += dx; - } - } - } - - private: - static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun, - const Token& t, int start, int end) - { - glyphRun->stringRange = { start, end }; - glyphRun->font = t.font; - glyphRun->colour = t.colour; - glyphLine.ascent = jmax (glyphLine.ascent, t.font.getAscent()); - glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent()); - glyphLine.runs.add (glyphRun); - } - - static int getCharacterType (juce_wchar c) noexcept - { - if (c == '\r' || c == '\n') - return 0; - - return CharacterFunctions::isWhitespace (c) ? 2 : 1; - } - - void appendText (const String& stringText, const Font& font, Colour colour) - { - auto t = stringText.getCharPointer(); - String currentString; - int lastCharType = 0; - - for (;;) - { - auto c = t.getAndAdvance(); - - if (c == 0) - break; - - auto charType = getCharacterType (c); - - if (charType == 0 || charType != lastCharType) - { - if (currentString.isNotEmpty()) - tokens.add (new Token (currentString, font, colour, - lastCharType == 2 || lastCharType == 0)); - - currentString = String::charToString (c); - - if (c == '\r' && *t == '\n') - currentString += t.getAndAdvance(); - } - else - { - currentString += c; - } - - lastCharType = charType; - } - - if (currentString.isNotEmpty()) - tokens.add (new Token (currentString, font, colour, lastCharType == 2)); - } - - void layoutRuns (float maxWidth, float extraLineSpacing, AttributedString::WordWrap wordWrap) - { - float x = 0, y = 0, h = 0; - int i; - - for (i = 0; i < tokens.size(); ++i) - { - auto& t = *tokens.getUnchecked (i); - t.area.setPosition (x, y); - t.line = totalLines; - x += t.area.getWidth(); - h = jmax (h, t.area.getHeight() + extraLineSpacing); - - auto* nextTok = tokens[i + 1]; - - if (nextTok == nullptr) - break; - - bool tokenTooLarge = (x + nextTok->area.getWidth() > maxWidth); - - if (t.isNewLine || ((! nextTok->isWhitespace) && (tokenTooLarge && wordWrap != AttributedString::none))) - { - setLastLineHeight (i + 1, h); - x = 0; - y += h; - h = 0; - ++totalLines; - } - } - - setLastLineHeight (jmin (i + 1, tokens.size()), h); - ++totalLines; - } - - void setLastLineHeight (int i, float height) noexcept - { - while (--i >= 0) - { - auto& tok = *tokens.getUnchecked (i); - - if (tok.line == totalLines) - tok.lineHeight = height; - else - break; - } - } - - void addTextRuns (const AttributedString& text) - { - auto numAttributes = text.getNumAttributes(); - tokens.ensureStorageAllocated (jmax (64, numAttributes)); - - for (int i = 0; i < numAttributes; ++i) - { - auto& attr = text.getAttribute (i); - - appendText (substring (text.getText(), attr.range), - attr.font, attr.colour); - } - } - - static String getTrimmedEndIfNotAllWhitespace (const String& s) - { - auto trimmed = s.trimEnd(); - - if (trimmed.isEmpty() && s.isNotEmpty()) - trimmed = s.replaceCharacters ("\r\n\t", " "); - - return trimmed; - } - - OwnedArray tokens; - int totalLines = 0; - - JUCE_DECLARE_NON_COPYABLE (TokenList) - }; + return Range (static_cast (r.getStart()), static_cast (r.getEnd())); +} + +static auto getFontsForRange (const detail::RangedValues& fonts) +{ + std::vector result; + result.reserve (fonts.size()); + + std::transform (fonts.begin(), + fonts.end(), + std::back_inserter (result), + [] (auto entry) { + return FontForRange { entry.range, entry.value }; + }); + + return result; +} + +static Range getInputRange (const ShapedText& st, Range glyphRange) +{ + if (glyphRange.isEmpty()) + { + jassertfalse; + return {}; + } + + const auto startInputRange = st.getTextRange (glyphRange.getStart()); + const auto endInputRange = st.getTextRange (glyphRange.getEnd() - 1); + + // The glyphRange is always in visual order and could have an opposite direction to the text + return { std::min (startInputRange.getStart(), endInputRange.getStart()), + std::max (startInputRange.getEnd(), endInputRange.getEnd()) }; +} + +static Range getLineInputRange (const ShapedText& st, int64 lineNumber) +{ + return getInputRange (st, ShapedText::Detail { &st }.getSimpleShapedText() + .getLineNumbers() + .getItem ((size_t) lineNumber).range); +} + +struct MaxFontAscentAndDescent +{ + float ascent{}, descent{}; +}; + +static MaxFontAscentAndDescent getMaxFontAscentAndDescentInEnclosingLine (const ShapedText& st, + Range lineChunkRange) +{ + const auto sst = ShapedText::Detail { &st }.getSimpleShapedText(); + + const auto lineRange = sst.getLineNumbers() + .getItemWithEnclosingRange (lineChunkRange.getStart())->range; + + const auto fonts = sst.getResolvedFonts().getIntersectionsWith (lineRange); + + MaxFontAscentAndDescent result; + + for (const auto& [r, font] : fonts) + { + result.ascent = std::max (result.ascent, font.getAscent()); + result.descent = std::max (result.descent, font.getDescent()); + } + + return result; } -//============================================================================== void TextLayout::createStandardLayout (const AttributedString& text) { - TextLayoutHelpers::TokenList l; - l.createLayout (text, *this); + detail::RangedValues fonts; + detail::RangedValues colours; + + for (auto i = 0, iMax = text.getNumAttributes(); i < iMax; ++i) + { + const auto& attribute = text.getAttribute (i); + const auto range = castTo (attribute.range); + fonts.set (range, attribute.font); + colours.set (range, attribute.colour); + } + + ShapedText shapedText { text.getText(), ShapedTextOptions{}.withFontsForRange (getFontsForRange (fonts)) + .withMaxWidth (width) + .withLanguage (SystemStats::getUserLanguage()) + .withTrailingWhitespacesShouldFit (false) + .withJustification (justification) }; + + std::optional lastLineNumber; + std::unique_ptr line; + + auto& jt = ShapedText::Detail { &shapedText }.getJustifiedText(); + jt.accessTogetherWith ([&] (Span glyphs, + Span> positions, + Font font, + Range glyphRange, + int64 lineNumber, + Colour colour) + { + if (std::exchange (lastLineNumber, lineNumber) != lineNumber) + { + if (line != nullptr) + addLine (std::move (line)); + + const auto ascentAndDescent = getMaxFontAscentAndDescentInEnclosingLine (shapedText, + glyphRange); + + line = std::make_unique (castTo (getLineInputRange (shapedText, lineNumber)), + positions[0], + ascentAndDescent.ascent, + ascentAndDescent.descent, + 0.0f, + 0); + } + + auto run = std::make_unique (castTo (getInputRange (shapedText, glyphRange)), 0); + + run->font = font; + run->colour = colour; + + for (decltype (glyphs.size()) i = 0, iMax = glyphs.size(); i < iMax; ++i) + { + if (glyphs[i].whitespace) + continue; + + run->glyphs.add ({ (int) glyphs[i].glyphId, positions[i] - line->lineOrigin, glyphs[i].advance.x }); + } + + line->runs.add (std::move (run)); + }, + colours); + + if (line != nullptr) + addLine (std::move (line)); } void TextLayout::recalculateSize() diff --git a/modules/juce_graphics/fonts/juce_TextLayout.h b/modules/juce_graphics/fonts/juce_TextLayout.h index 6fd9771a08..9bdd797555 100644 --- a/modules/juce_graphics/fonts/juce_TextLayout.h +++ b/modules/juce_graphics/fonts/juce_TextLayout.h @@ -53,7 +53,7 @@ private: class DereferencingIterator { public: - using value_type = std::remove_reference_t())>; + using value_type = std::remove_reference_t())>; using difference_type = typename std::iterator_traits::difference_type; using pointer = value_type*; using reference = value_type&; @@ -94,7 +94,7 @@ private: DereferencingIterator operator++ (int) const { DereferencingIterator copy (*this); ++(*this); return copy; } DereferencingIterator operator-- (int) const { DereferencingIterator copy (*this); --(*this); return copy; } - reference operator* () const { return **iterator; } + reference operator*() const { return **iterator; } pointer operator->() const { return *iterator; } private: