/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ TextLayout::Glyph::Glyph (const int glyph, Point anch, float w) noexcept : glyphCode (glyph), anchor (anch), width (w) { } TextLayout::Glyph::Glyph (const Glyph& other) noexcept : glyphCode (other.glyphCode), anchor (other.anchor), width (other.width) { } TextLayout::Glyph& TextLayout::Glyph::operator= (const Glyph& other) noexcept { glyphCode = other.glyphCode; anchor = other.anchor; width = other.width; return *this; } TextLayout::Glyph::~Glyph() noexcept {} //============================================================================== TextLayout::Run::Run() noexcept : colour (0xff000000) { } TextLayout::Run::Run (Range range, const int numGlyphsToPreallocate) : colour (0xff000000), stringRange (range) { glyphs.ensureStorageAllocated (numGlyphsToPreallocate); } TextLayout::Run::Run (const Run& other) : font (other.font), colour (other.colour), glyphs (other.glyphs), stringRange (other.stringRange) { } TextLayout::Run::~Run() noexcept {} //============================================================================== TextLayout::Line::Line() noexcept : ascent (0.0f), descent (0.0f), leading (0.0f) { } TextLayout::Line::Line (Range range, Point o, float asc, float desc, float lead, int numRunsToPreallocate) : stringRange (range), lineOrigin (o), ascent (asc), descent (desc), leading (lead) { runs.ensureStorageAllocated (numRunsToPreallocate); } TextLayout::Line::Line (const Line& other) : stringRange (other.stringRange), lineOrigin (other.lineOrigin), ascent (other.ascent), descent (other.descent), leading (other.leading) { runs.addCopiesOf (other.runs); } TextLayout::Line::~Line() noexcept { } Range TextLayout::Line::getLineBoundsX() const noexcept { Range range; bool isFirst = true; for (int i = runs.size(); --i >= 0;) { const Run& run = *runs.getUnchecked(i); if (run.glyphs.size() > 0) { float minX = run.glyphs.getReference(0).anchor.x; float maxX = minX; for (int j = run.glyphs.size(); --j >= 0;) { const Glyph& glyph = run.glyphs.getReference (j); const float x = glyph.anchor.x; minX = jmin (minX, x); maxX = jmax (maxX, x + glyph.width); } if (isFirst) { isFirst = false; range = Range (minX, maxX); } else { range = range.getUnionWith (Range (minX, maxX)); } } } return range + lineOrigin.x; } Range TextLayout::Line::getLineBoundsY() const noexcept { return Range (lineOrigin.y - ascent, lineOrigin.y + descent); } Rectangle TextLayout::Line::getLineBounds() const noexcept { const Range x (getLineBoundsX()), y (getLineBoundsY()); return Rectangle (x.getStart(), y.getStart(), x.getLength(), y.getLength()); } //============================================================================== TextLayout::TextLayout() : width (0), height (0), justification (Justification::topLeft) { } TextLayout::TextLayout (const TextLayout& other) : width (other.width), height (other.height), justification (other.justification) { lines.addCopiesOf (other.lines); } #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS TextLayout::TextLayout (TextLayout&& other) noexcept : lines (static_cast&&> (other.lines)), width (other.width), height (other.height), justification (other.justification) { } TextLayout& TextLayout::operator= (TextLayout&& other) noexcept { lines = static_cast&&> (other.lines); width = other.width; height = other.height; justification = other.justification; return *this; } #endif TextLayout& TextLayout::operator= (const TextLayout& other) { width = other.width; height = other.height; justification = other.justification; lines.clear(); lines.addCopiesOf (other.lines); return *this; } TextLayout::~TextLayout() { } TextLayout::Line& TextLayout::getLine (const int index) const { return *lines.getUnchecked (index); } void TextLayout::ensureStorageAllocated (int numLinesNeeded) { lines.ensureStorageAllocated (numLinesNeeded); } void TextLayout::addLine (Line* line) { lines.add (line); } void TextLayout::draw (Graphics& g, const Rectangle& area) const { const Point origin (justification.appliedToRectangle (Rectangle (width, getHeight()), area).getPosition()); LowLevelGraphicsContext& context = g.getInternalContext(); for (int i = 0; i < lines.size(); ++i) { const Line& line = getLine (i); const Point lineOrigin (origin + line.lineOrigin); for (int j = 0; j < line.runs.size(); ++j) { const Run& run = *line.runs.getUnchecked (j); context.setFont (run.font); context.setFill (run.colour); for (int k = 0; k < run.glyphs.size(); ++k) { const Glyph& glyph = run.glyphs.getReference (k); context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x, lineOrigin.y + glyph.anchor.y)); } } } } void TextLayout::createLayout (const AttributedString& text, float maxWidth) { createLayout (text, maxWidth, 1.0e7f); } void TextLayout::createLayout (const AttributedString& text, float maxWidth, float maxHeight) { lines.clear(); width = maxWidth; height = maxHeight; justification = text.getJustification(); if (! createNativeLayout (text)) createStandardLayout (text); recalculateSize (text); } void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth) { createLayoutWithBalancedLineLengths (text, maxWidth, 1.0e7f); } void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth, float maxHeight) { const float minimumWidth = maxWidth / 2.0f; float bestWidth = maxWidth; float bestLineProportion = 0.0f; while (maxWidth > minimumWidth) { createLayout (text, maxWidth, maxHeight); if (getNumLines() < 2) return; const float line1 = lines.getUnchecked (lines.size() - 1)->getLineBoundsX().getLength(); const float line2 = lines.getUnchecked (lines.size() - 2)->getLineBoundsX().getLength(); const float shortestLine = jmin (line1, line2); const float prop = (shortestLine > 0) ? jmax (line1, line2) / shortestLine : 1.0f; if (prop > 0.9f) return; if (prop > bestLineProportion) { bestLineProportion = prop; bestWidth = maxWidth; } maxWidth -= 10.0f; } if (bestWidth != maxWidth) createLayout (text, bestWidth, maxHeight); } //============================================================================== namespace TextLayoutHelpers { struct FontAndColour { FontAndColour (const Font* f) noexcept : font (f), colour (0xff000000) {} const Font* font; Colour colour; bool operator!= (const FontAndColour& other) const noexcept { return (font != other.font && *font != *other.font) || colour != other.colour; } }; struct RunAttribute { RunAttribute (const FontAndColour& fc, const Range r) noexcept : fontAndColour (fc), range (r) {} FontAndColour fontAndColour; Range range; }; struct Token { Token (const String& t, const Font& f, Colour c, const 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; private: Token& operator= (const Token&); }; class TokenList { public: TokenList() noexcept : totalLines (0) {} void createLayout (const AttributedString& text, TextLayout& layout) { tokens.ensureStorageAllocated (64); layout.ensureStorageAllocated (totalLines); addTextRuns (text); layoutRuns (layout.getWidth()); int charPosition = 0; int lineStartPosition = 0; int runStartPosition = 0; ScopedPointer currentLine; ScopedPointer currentRun; bool needToSetLineOrigin = true; for (int i = 0; i < tokens.size(); ++i) { const Token& t = *tokens.getUnchecked (i); Array newGlyphs; Array xOffsets; t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets); if (currentRun == nullptr) currentRun = new TextLayout::Run(); if (currentLine == nullptr) currentLine = new TextLayout::Line(); if (newGlyphs.size() > 0) { currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size()); const Point tokenOrigin (t.area.getPosition().translated (0, t.font.getAscent())); if (needToSetLineOrigin) { needToSetLineOrigin = false; currentLine->lineOrigin = tokenOrigin; } const Point glyphOffset (tokenOrigin - currentLine->lineOrigin); for (int j = 0; j < newGlyphs.size(); ++j) { const float x = xOffsets.getUnchecked (j); currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked(j), glyphOffset.translated (x, 0), xOffsets.getUnchecked (j + 1) - x)); } charPosition += newGlyphs.size(); } if (t.isWhitespace || t.isNewLine) ++charPosition; const Token* const nextToken = tokens [i + 1]; if (nextToken == nullptr) // this is the last token { addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition); currentLine->stringRange = Range (lineStartPosition, charPosition); if (! needToSetLineOrigin) layout.addLine (currentLine.release()); needToSetLineOrigin = true; } else { 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 = new TextLayout::Run(); addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition); currentLine->stringRange = Range (lineStartPosition, charPosition); if (! needToSetLineOrigin) layout.addLine (currentLine.release()); runStartPosition = charPosition; lineStartPosition = charPosition; needToSetLineOrigin = true; } } } if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0) { const float totalW = layout.getWidth(); const bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0; for (int i = 0; i < layout.getNumLines(); ++i) { float dx = totalW - layout.getLine(i).getLineBoundsX().getLength(); if (isCentred) dx /= 2.0f; layout.getLine(i).lineOrigin.x += dx; } } } private: static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun, const Token& t, const int start, const int end) { glyphRun->stringRange = Range (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 (const juce_wchar c) noexcept { if (c == '\r' || c == '\n') return 0; return CharacterFunctions::isWhitespace (c) ? 2 : 1; } void appendText (const AttributedString& text, const Range stringRange, const Font& font, Colour colour) { const String stringText (text.getText().substring (stringRange.getStart(), stringRange.getEnd())); String::CharPointerType t (stringText.getCharPointer()); String currentString; int lastCharType = 0; for (;;) { const juce_wchar c = t.getAndAdvance(); if (c == 0) break; const int 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 (const float maxWidth) { float x = 0, y = 0, h = 0; int i; for (i = 0; i < tokens.size(); ++i) { Token& t = *tokens.getUnchecked(i); t.area.setPosition (x, y); t.line = totalLines; x += t.area.getWidth(); h = jmax (h, t.area.getHeight()); const Token* const nextTok = tokens[i + 1]; if (nextTok == nullptr) break; if (t.isNewLine || ((! nextTok->isWhitespace) && x + nextTok->area.getWidth() > maxWidth)) { setLastLineHeight (i + 1, h); x = 0; y += h; h = 0; ++totalLines; } } setLastLineHeight (jmin (i + 1, tokens.size()), h); ++totalLines; } void setLastLineHeight (int i, const float height) noexcept { while (--i >= 0) { Token& tok = *tokens.getUnchecked (i); if (tok.line == totalLines) tok.lineHeight = height; else break; } } void addTextRuns (const AttributedString& text) { Font defaultFont; Array runAttributes; { const int stringLength = text.getText().length(); int rangeStart = 0; FontAndColour lastFontAndColour (&defaultFont); // Iterate through every character in the string for (int i = 0; i < stringLength; ++i) { FontAndColour newFontAndColour (&defaultFont); const int numCharacterAttributes = text.getNumAttributes(); for (int j = 0; j < numCharacterAttributes; ++j) { const AttributedString::Attribute& attr = *text.getAttribute (j); if (attr.range.contains (i)) { if (const Font* f = attr.getFont()) newFontAndColour.font = f; if (const Colour* c = attr.getColour()) newFontAndColour.colour = *c; } } if (i > 0 && newFontAndColour != lastFontAndColour) { runAttributes.add (RunAttribute (lastFontAndColour, Range (rangeStart, i))); rangeStart = i; } lastFontAndColour = newFontAndColour; } if (rangeStart < stringLength) runAttributes.add (RunAttribute (lastFontAndColour, Range (rangeStart, stringLength))); } for (int i = 0; i < runAttributes.size(); ++i) { const RunAttribute& r = runAttributes.getReference(i); appendText (text, r.range, *(r.fontAndColour.font), r.fontAndColour.colour); } } static String getTrimmedEndIfNotAllWhitespace (const String& s) { String trimmed (s.trimEnd()); if (trimmed.isEmpty() && ! s.isEmpty()) trimmed = s.replaceCharacters ("\r\n\t", " "); return trimmed; } OwnedArray tokens; int totalLines; JUCE_DECLARE_NON_COPYABLE (TokenList) }; } //============================================================================== void TextLayout::createStandardLayout (const AttributedString& text) { TextLayoutHelpers::TokenList l; l.createLayout (text, *this); } void TextLayout::recalculateSize (const AttributedString& text) { if (lines.size() > 0 && text.getReadingDirection() != AttributedString::rightToLeft) { Rectangle bounds (lines.getFirst()->getLineBounds()); for (int i = lines.size(); --i > 0;) bounds = bounds.getUnion (lines.getUnchecked(i)->getLineBounds()); for (int i = lines.size(); --i >= 0;) lines.getUnchecked(i)->lineOrigin.x -= bounds.getX(); width = bounds.getWidth(); height = bounds.getHeight(); } else { width = 0; height = 0; } }