From 58db7eb88047a79a43c41f795b838b4aae5b5540 Mon Sep 17 00:00:00 2001 From: jules Date: Fri, 25 Nov 2011 14:25:12 +0000 Subject: [PATCH] Complete rewrite of the TextLayout class, to provide better support for native platform layout functions. It now works with the AttributedString class, to provide a pre-formatted AttributedString that can be drawn. --- .../jucer_ProjectInformationComponent.cpp | 13 +- .../Source/Utility/jucer_MiscUtilities.cpp | 14 +- .../Source/Utility/jucer_MiscUtilities.h | 1 - extras/the jucer/src/ui/jucer_PrefsPanel.cpp | 90 +-- .../fonts/juce_AttributedString.cpp | 550 ++------------ .../fonts/juce_AttributedString.h | 177 +---- .../juce_graphics/fonts/juce_TextLayout.cpp | 704 ++++++++++++------ modules/juce_graphics/fonts/juce_TextLayout.h | 189 ++--- modules/juce_graphics/juce_graphics.h | 2 +- .../juce_graphics/native/juce_mac_Fonts.mm | 227 +++--- .../juce_win32_DirectWriteTypeLayout.cpp | 51 +- .../lookandfeel/juce_LookAndFeel.cpp | 36 +- .../windows/juce_AlertWindow.cpp | 27 +- .../windows/juce_AlertWindow.h | 1 + .../misc/juce_BubbleMessageComponent.cpp | 27 +- .../misc/juce_BubbleMessageComponent.h | 1 + 16 files changed, 952 insertions(+), 1158 deletions(-) diff --git a/extras/Introjucer/Source/Project/jucer_ProjectInformationComponent.cpp b/extras/Introjucer/Source/Project/jucer_ProjectInformationComponent.cpp index 84e6ad2e4e..af796262be 100644 --- a/extras/Introjucer/Source/Project/jucer_ProjectInformationComponent.cpp +++ b/extras/Introjucer/Source/Project/jucer_ProjectInformationComponent.cpp @@ -730,16 +730,16 @@ public: void paint (Graphics& g) { - g.setColour (Colour::greyLevel (0.15f)); - g.setFont (13.0f); + AttributedString s; + s.setJustification (Justification::centredLeft); + s.append (lastTip, Font (14.0f), Colour::greyLevel (0.15f)); TextLayout tl; - tl.appendText (lastTip, Font (14.0f)); - tl.layout (getWidth() - 10, Justification::left, true); // try to make it look nice + tl.createLayoutWithBalancedLineLengths (s, getWidth() - 10.0f); if (tl.getNumLines() > 3) - tl.layout (getWidth() - 10, Justification::left, false); // too big, so just squash it in.. + tl.createLayout (s, getWidth() - 10.0f); - tl.drawWithin (g, 0, 0, getWidth(), getHeight(), Justification::centred); + tl.draw (g, getLocalBounds().toFloat()); } void timerCallback() @@ -785,7 +785,6 @@ private: return String::empty; } - TextLayout layout; Component* lastComp; String lastTip; }; diff --git a/extras/Introjucer/Source/Utility/jucer_MiscUtilities.cpp b/extras/Introjucer/Source/Utility/jucer_MiscUtilities.cpp index 4d813e6308..593082839c 100644 --- a/extras/Introjucer/Source/Utility/jucer_MiscUtilities.cpp +++ b/extras/Introjucer/Source/Utility/jucer_MiscUtilities.cpp @@ -231,18 +231,16 @@ PropertyPanelWithTooltips::~PropertyPanelWithTooltips() void PropertyPanelWithTooltips::paint (Graphics& g) { - g.setColour (Colour::greyLevel (0.15f)); - g.setFont (13.0f); + AttributedString s; + s.setJustification (Justification::centredLeft); + s.append (lastTip, Font (14.0f), Colour::greyLevel (0.15f)); TextLayout tl; - tl.appendText (lastTip, Font (14.0f)); - tl.layout (getWidth() - 10, Justification::left, true); // try to make it look nice + tl.createLayoutWithBalancedLineLengths (s, getWidth() - 10.0f); if (tl.getNumLines() > 3) - tl.layout (getWidth() - 10, Justification::left, false); // too big, so just squash it in.. + tl.createLayout (s, getWidth() - 10.0f); - Rectangle r (getTipArea()); - tl.drawWithin (g, r.getX(), r.getY(), r.getWidth(), r.getHeight(), - Justification::bottomLeft); + tl.draw (g, getLocalBounds().toFloat()); } void PropertyPanelWithTooltips::resized() diff --git a/extras/Introjucer/Source/Utility/jucer_MiscUtilities.h b/extras/Introjucer/Source/Utility/jucer_MiscUtilities.h index 978cfcf499..0c01be2f45 100644 --- a/extras/Introjucer/Source/Utility/jucer_MiscUtilities.h +++ b/extras/Introjucer/Source/Utility/jucer_MiscUtilities.h @@ -62,7 +62,6 @@ public: private: PropertyPanel panel; - TextLayout layout; Component* lastComp; String lastTip; diff --git a/extras/the jucer/src/ui/jucer_PrefsPanel.cpp b/extras/the jucer/src/ui/jucer_PrefsPanel.cpp index fb2f59cebe..b0b00c8511 100644 --- a/extras/the jucer/src/ui/jucer_PrefsPanel.cpp +++ b/extras/the jucer/src/ui/jucer_PrefsPanel.cpp @@ -29,88 +29,80 @@ //============================================================================== class MiscPage : public Component { - FilenameComponent* templateDir; - public: MiscPage() + : templateDir ("C++ template folder:", + StoredSettings::getInstance()->getTemplatesDir(), + true, true, false, + "*.*", String::empty, + "(select the directory containing template .cpp and .h files)"), + label (String::empty, templateDir.getName()) { - addAndMakeVisible (templateDir - = new FilenameComponent ("C++ template folder:", - StoredSettings::getInstance()->getTemplatesDir(), - true, - true, - false, - "*.*", - String::empty, - "(select the directory containing template .cpp and .h files)")); - - (new Label (String::empty, templateDir->getName()))->attachToComponent (templateDir, true); + addAndMakeVisible (&templateDir); + label.attachToComponent (&templateDir, true); } ~MiscPage() { - StoredSettings::getInstance()->setTemplatesDir (templateDir->getCurrentFile()); - - deleteAllChildren(); + StoredSettings::getInstance()->setTemplatesDir (templateDir.getCurrentFile()); } void resized() { - templateDir->setBounds (150, 16, getWidth() - 160, 22); + templateDir.setBounds (150, 16, getWidth() - 160, 22); } + +private: + FilenameComponent templateDir; + Label label; }; //============================================================================== class AboutPage : public Component { - HyperlinkButton* link; - Image logo; - TextLayout text1, text2; - public: AboutPage() + : link ("www.rawmaterialsoftware.com/juce", + URL ("http://www.rawmaterialsoftware.com/juce")), + logo (ImageCache::getFromMemory (BinaryData::jules_jpg, BinaryData::jules_jpgSize)) { - logo = ImageCache::getFromMemory (BinaryData::jules_jpg, BinaryData::jules_jpgSize); - - text1.appendText ("Programmer Julian Storer, seen here demonstrating a beard designed to " - "gain approval from the Linux programming community. Each hair of the beard " - "represents one line of source code from the ", Font (13.0f)); - text1.appendText ("Jucer", Font (13.0f, Font::bold)); - text1.appendText (" component design tool.", Font (13.0f)); - - text2.appendText ("Jucer v" + JUCEApplication::getInstance()->getApplicationVersion() - + ", " + SystemStats::getJUCEVersion(), Font (14.0f, Font::bold)); - - addAndMakeVisible (link = new HyperlinkButton ("www.rawmaterialsoftware.com/juce", - URL ("http://www.rawmaterialsoftware.com/juce"))); - link->setFont (Font (10.0f, Font::bold | Font::underlined), true); - } - - ~AboutPage() - { - deleteAllChildren(); + text1.setJustification (Justification::centredTop); + text1.append ("Programmer Julian Storer, seen here demonstrating a beard designed to " + "gain approval from the Linux programming community. Each hair of the beard " + "represents one line of source code from the ", Font (13.0f)); + text1.append ("Jucer", Font (13.0f, Font::bold)); + text1.append (" component design tool.", Font (13.0f)); + + text2.setJustification (Justification::centred); + text2.append ("Jucer v" + JUCEApplication::getInstance()->getApplicationVersion() + + ", " + SystemStats::getJUCEVersion(), Font (12.0f, Font::bold)); + + addAndMakeVisible (&link); + link.setFont (Font (10.0f, Font::bold | Font::underlined), true); } void paint (Graphics& g) { g.fillAll (Colour (0xffebebeb)); - g.drawImageWithin (logo, 0, 4, getWidth(), getHeight() - 134, + g.drawImageWithin (logo, 0, 4, getWidth(), getHeight() - 144, RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize, false); - text1.drawWithin (g, 0, getHeight() - 120, getWidth(), 100, Justification::centredTop); - text2.drawWithin (g, 0, getHeight() - 50, getWidth(), 100, Justification::centredTop); + text1.draw (g, Rectangle (12, getHeight() - 130, getWidth() - 24, 100).toFloat()); + text2.draw (g, Rectangle (12, getHeight() - 50, getWidth() - 24, 20).toFloat()); } void resized() { - text1.layout (getWidth() - 24, Justification::topLeft, false); - text2.layout (getWidth() - 24, Justification::centred, false); - - link->setSize (100, 22); - link->changeWidthToFitText(); - link->setTopLeftPosition ((getWidth() - link->getWidth()) / 2, getHeight() - link->getHeight() - 10); + link.setSize (100, 22); + link.changeWidthToFitText(); + link.setTopLeftPosition ((getWidth() - link.getWidth()) / 2, getHeight() - link.getHeight() - 10); } + +private: + HyperlinkButton link; + Image logo; + AttributedString text1, text2; }; diff --git a/modules/juce_graphics/fonts/juce_AttributedString.cpp b/modules/juce_graphics/fonts/juce_AttributedString.cpp index 8389d6912b..4630d2a061 100644 --- a/modules/juce_graphics/fonts/juce_AttributedString.cpp +++ b/modules/juce_graphics/fonts/juce_AttributedString.cpp @@ -89,546 +89,134 @@ AttributedString& AttributedString::operator= (const AttributedString& other) return *this; } -AttributedString::~AttributedString() {} - -void AttributedString::setText (const String& other) -{ - text = other; -} - -void AttributedString::setJustification (const Justification& newJustification) noexcept -{ - justification = newJustification; -} - -void AttributedString::setWordWrap (WordWrap newWordWrap) noexcept -{ - wordWrap = newWordWrap; -} - -void AttributedString::setReadingDirection (ReadingDirection newReadingDirection) noexcept -{ - readingDirection = newReadingDirection; -} - -void AttributedString::setLineSpacing (const float newLineSpacing) noexcept -{ - lineSpacing = newLineSpacing; -} - -void AttributedString::setColour (const Range& range, const Colour& colour) -{ - attributes.add (new Attribute (range, colour)); -} - -void AttributedString::setFont (const Range& range, const Font& font) -{ - attributes.add (new Attribute (range, font)); -} - -void AttributedString::draw (Graphics& g, const Rectangle& area) const -{ - if (text.isNotEmpty() && g.clipRegionIntersects (area.getSmallestIntegerContainer())) - { - if (! g.getInternalContext()->drawTextLayout (*this, area)) - { - GlyphLayout layout; - layout.setText (*this, area.getWidth()); - layout.draw (g, area); - } - } -} - - -//============================================================================== -GlyphLayout::Glyph::Glyph (const int glyphCode_, const Point& anchor_) noexcept - : glyphCode (glyphCode_), anchor (anchor_) -{ -} - -GlyphLayout::Glyph::~Glyph() {} - -//============================================================================== -GlyphLayout::Run::Run() - : colour (0xff000000) -{ -} - -GlyphLayout::Run::Run (const Range& range, const int numGlyphsToPreallocate) - : stringRange (range), colour (0xff000000) +#if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS +AttributedString::AttributedString (AttributedString&& other) noexcept + : text (static_cast (other.text)), + lineSpacing (other.lineSpacing), + justification (other.justification), + wordWrap (other.wordWrap), + readingDirection (other.readingDirection), + attributes (static_cast &&> (other.attributes)) { - glyphs.ensureStorageAllocated (numGlyphsToPreallocate); } -GlyphLayout::Run::~Run() {} - -GlyphLayout::Glyph& GlyphLayout::Run::getGlyph (const int index) const +AttributedString& AttributedString::operator= (AttributedString&& other) noexcept { - return *glyphs.getUnchecked (index); + text = static_cast (other.text); + lineSpacing = other.lineSpacing; + justification = other.justification; + wordWrap = other.wordWrap; + readingDirection = other.readingDirection; + attributes = static_cast &&> (other.attributes); + return *this; } +#endif -void GlyphLayout::Run::ensureStorageAllocated (int numGlyphsNeeded) -{ - glyphs.ensureStorageAllocated (numGlyphsNeeded); -} +AttributedString::~AttributedString() {} -void GlyphLayout::Run::setStringRange (const Range& newStringRange) noexcept +void AttributedString::setText (const String& other) { - stringRange = newStringRange; + text = other; } -void GlyphLayout::Run::setFont (const Font& newFont) +void AttributedString::append (const String& textToAppend) { - font = newFont; + text += textToAppend; } -void GlyphLayout::Run::setColour (const Colour& newColour) noexcept +void AttributedString::append (const String& textToAppend, const Font& font) { - colour = newColour; -} + const int oldLength = text.length(); + const int newLength = textToAppend.length(); -void GlyphLayout::Run::addGlyph (Glyph* glyph) -{ - glyphs.add (glyph); + text += textToAppend; + setFont (Range (oldLength, oldLength + newLength), font); } -//============================================================================== -GlyphLayout::Line::Line() noexcept - : ascent (0.0f), descent (0.0f), leading (0.0f) +void AttributedString::append (const String& textToAppend, const Colour& colour) { -} + const int oldLength = text.length(); + const int newLength = textToAppend.length(); -GlyphLayout::Line::Line (const Range& stringRange_, const Point& lineOrigin_, - const float ascent_, const float descent_, const float leading_, - const int numRunsToPreallocate) - : stringRange (stringRange_), lineOrigin (lineOrigin_), - ascent (ascent_), descent (descent_), leading (leading_) -{ - runs.ensureStorageAllocated (numRunsToPreallocate); + text += textToAppend; + setColour (Range (oldLength, oldLength + newLength), colour); } -GlyphLayout::Line::~Line() +void AttributedString::append (const String& textToAppend, const Font& font, const Colour& colour) { -} + const int oldLength = text.length(); + const int newLength = textToAppend.length(); -GlyphLayout::Run& GlyphLayout::Line::getRun (const int index) const noexcept -{ - return *runs.getUnchecked (index); + text += textToAppend; + setFont (Range (oldLength, oldLength + newLength), font); + setColour (Range (oldLength, oldLength + newLength), colour); } -void GlyphLayout::Line::setStringRange (const Range& newStringRange) noexcept +void AttributedString::clear() { - stringRange = newStringRange; + text = String::empty; + attributes.clear(); } -void GlyphLayout::Line::setLineOrigin (const Point& newLineOrigin) noexcept -{ - lineOrigin = newLineOrigin; -} - -void GlyphLayout::Line::setLeading (float newLeading) noexcept -{ - leading = newLeading; -} - -void GlyphLayout::Line::increaseAscentDescent (float newAscent, float newDescent) noexcept +void AttributedString::setJustification (const Justification& newJustification) noexcept { - ascent = jmax (ascent, newAscent); - descent = jmax (descent, newDescent); + justification = newJustification; } -void GlyphLayout::Line::addRun (Run* run) +void AttributedString::setWordWrap (WordWrap newWordWrap) noexcept { - runs.add (run); + wordWrap = newWordWrap; } -//============================================================================== -GlyphLayout::GlyphLayout() - : width (0), justification (Justification::topLeft) +void AttributedString::setReadingDirection (ReadingDirection newReadingDirection) noexcept { + readingDirection = newReadingDirection; } -GlyphLayout::~GlyphLayout() +void AttributedString::setLineSpacing (const float newLineSpacing) noexcept { + lineSpacing = newLineSpacing; } -void GlyphLayout::setText (const AttributedString& text, float maxWidth) +void AttributedString::setColour (const Range& range, const Colour& colour) { - lines.clear(); - width = maxWidth; - justification = text.getJustification(); - - if (! createNativeLayout (text)) - createStandardLayout (text); + attributes.add (new Attribute (range, colour)); } -float GlyphLayout::getHeight() const noexcept +void AttributedString::setColour (const Colour& colour) { - const Line* const lastLine = lines.getLast(); + for (int i = attributes.size(); --i >= 0;) + if (attributes.getUnchecked(i)->getColour() != nullptr) + attributes.remove (i); - return lastLine != nullptr ? lastLine->getLineOrigin().getY() + lastLine->getDescent() - : 0; + setColour (Range (0, text.length()), colour); } -GlyphLayout::Line& GlyphLayout::getLine (const int index) const +void AttributedString::setFont (const Range& range, const Font& font) { - return *lines[index]; + attributes.add (new Attribute (range, font)); } -void GlyphLayout::ensureStorageAllocated (int numLinesNeeded) +void AttributedString::setFont (const Font& font) { - lines.ensureStorageAllocated (numLinesNeeded); -} + for (int i = attributes.size(); --i >= 0;) + if (attributes.getUnchecked(i)->getFont() != nullptr) + attributes.remove (i); -void GlyphLayout::addLine (Line* line) -{ - lines.add (line); + setFont (Range (0, text.length()), font); } -void GlyphLayout::draw (Graphics& g, const Rectangle& area) const +void AttributedString::draw (Graphics& g, const Rectangle& area) const { - const Point origin (justification.appliedToRectangle (Rectangle (0, 0, width, getHeight()), area).getPosition()); - - LowLevelGraphicsContext& context = *g.getInternalContext(); - - for (int i = 0; i < getNumLines(); ++i) + if (text.isNotEmpty() && g.clipRegionIntersects (area.getSmallestIntegerContainer())) { - const Line& line = getLine (i); - const Point lineOrigin (origin + line.getLineOrigin()); - - for (int j = 0; j < line.getNumRuns(); ++j) + if (! g.getInternalContext()->drawTextLayout (*this, area)) { - const Run& run = line.getRun (j); - context.setFont (run.getFont()); - context.setFill (run.getColour()); - - for (int k = 0; k < run.getNumGlyphs(); ++k) - { - const Glyph& glyph = run.getGlyph (k); - context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x, - lineOrigin.y + glyph.anchor.y)); - } + TextLayout layout; + layout.createLayout (*this, area.getWidth()); + layout.draw (g, area); } } } -//============================================================================== -namespace GlyphLayoutHelpers -{ - struct FontAndColour - { - FontAndColour (const Font* font_) noexcept : font (font_), 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& fontAndColour_, const Range& range_) noexcept - : fontAndColour (fontAndColour_), range (range_) - {} - - FontAndColour fontAndColour; - Range range; - }; - - struct Token - { - Token (const String& t, const Font& f, const Colour& c, const bool isWhitespace_) - : text (t), font (f), colour (c), - area (font.getStringWidth (t), roundToInt (f.getHeight())), - isWhitespace (isWhitespace_), - isNewLine (t.containsChar ('\n') || t.containsChar ('\r')) - {} - - const String text; - const Font font; - const Colour colour; - Rectangle area; - int line, lineHeight; - const bool isWhitespace, isNewLine; - - private: - Token& operator= (const Token&); - }; - - class TokenList - { - public: - TokenList() noexcept : totalLines (0) {} - - void createLayout (const AttributedString& text, GlyphLayout& glyphLayout) - { - tokens.ensureStorageAllocated (64); - glyphLayout.ensureStorageAllocated (totalLines); - - addTextRuns (text); - - layout ((int) glyphLayout.getWidth()); - - int charPosition = 0; - int lineStartPosition = 0; - int runStartPosition = 0; - - GlyphLayout::Line* glyphLine = new GlyphLayout::Line(); - GlyphLayout::Run* glyphRun = new GlyphLayout::Run(); - - for (int i = 0; i < tokens.size(); ++i) - { - const Token* const t = tokens.getUnchecked (i); - const Point tokenPos (t->area.getPosition().toFloat()); - - Array newGlyphs; - Array xOffsets; - t->font.getGlyphPositions (t->text.trimEnd(), newGlyphs, xOffsets); - - glyphRun->ensureStorageAllocated (glyphRun->getNumGlyphs() + newGlyphs.size()); - - for (int j = 0; j < newGlyphs.size(); ++j) - { - if (charPosition == lineStartPosition) - glyphLine->setLineOrigin (tokenPos.translated (0, t->font.getAscent())); - - glyphRun->addGlyph (new GlyphLayout::Glyph (newGlyphs.getUnchecked(j), - Point (tokenPos.getX() + xOffsets.getUnchecked (j), 0))); - ++charPosition; - } - - if (t->isWhitespace || t->isNewLine) - ++charPosition; - - const Token* const nextToken = tokens [i + 1]; - - if (nextToken == nullptr) // this is the last token - { - addRun (glyphLine, glyphRun, t, runStartPosition, charPosition); - glyphLine->setStringRange (Range (lineStartPosition, charPosition)); - glyphLayout.addLine (glyphLine); - } - else - { - if (t->font != nextToken->font || t->colour != nextToken->colour) - { - addRun (glyphLine, glyphRun, t, runStartPosition, charPosition); - runStartPosition = charPosition; - glyphRun = new GlyphLayout::Run(); - } - - if (t->line != nextToken->line) - { - addRun (glyphLine, glyphRun, t, runStartPosition, charPosition); - glyphLine->setStringRange (Range (lineStartPosition, charPosition)); - glyphLayout.addLine (glyphLine); - - runStartPosition = charPosition; - lineStartPosition = charPosition; - glyphLine = new GlyphLayout::Line(); - glyphRun = new GlyphLayout::Run(); - } - } - } - - if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0) - { - const int totalW = (int) glyphLayout.getWidth(); - - for (int i = 0; i < totalLines; ++i) - { - const int lineW = getLineWidth (i); - float dx = 0; - - if ((text.getJustification().getFlags() & Justification::right) != 0) - dx = (float) (totalW - lineW); - else - dx = (totalW - lineW) / 2.0f; - - GlyphLayout::Line& glyphLine = glyphLayout.getLine (i); - glyphLine.setLineOrigin (glyphLine.getLineOrigin().translated (dx, 0)); - } - } - } - - private: - static void addRun (GlyphLayout::Line* glyphLine, GlyphLayout::Run* glyphRun, - const Token* const t, const int start, const int end) - { - glyphRun->setStringRange (Range (start, end)); - glyphRun->setFont (t->font); - glyphRun->setColour (t->colour); - glyphLine->increaseAscentDescent (t->font.getAscent(), t->font.getDescent()); - glyphLine->addRun (glyphRun); - } - - void appendText (const AttributedString& text, const Range& stringRange, - const Font& font, const Colour& colour) - { - 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; - - int charType; - if (c == '\r' || c == '\n') - charType = 0; - else if (CharacterFunctions::isWhitespace (c)) - charType = 2; - else - charType = 1; - - 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 layout (const int maxWidth) - { - int x = 0, y = 0, h = 0; - int i; - - for (i = 0; i < tokens.size(); ++i) - { - Token* const t = tokens.getUnchecked(i); - t->area.setPosition (x, y); - t->line = totalLines; - x += t->area.getWidth(); - h = jmax (h, t->area.getHeight()); - - const Token* nextTok = tokens[i + 1]; - - if (nextTok == 0) - 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 int height) noexcept - { - while (--i >= 0) - { - Token* const tok = tokens.getUnchecked (i); - - if (tok->line == totalLines) - tok->lineHeight = height; - else - break; - } - } - - int getLineWidth (const int lineNumber) const noexcept - { - int maxW = 0; - - for (int i = tokens.size(); --i >= 0;) - { - const Token* const t = tokens.getUnchecked (i); - - if (t->line == lineNumber && ! t->isWhitespace) - maxW = jmax (maxW, t->area.getRight()); - } - - return maxW; - } - - void addTextRuns (const AttributedString& text) - { - Font defaultFont; - Array runAttributes; - - { - const int stringLength = text.getText().length(); - int rangeStart = 0; - FontAndColour lastFontAndColour (nullptr); - - // 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* const attr = text.getAttribute (j); - - // Check if the current character falls within the range of a font attribute - if (attr->getFont() != nullptr && (i >= attr->range.getStart()) && (i < attr->range.getEnd())) - newFontAndColour.font = attr->getFont(); - - // Check if the current character falls within the range of a foreground colour attribute - if (attr->getColour() != nullptr && (i >= attr->range.getStart()) && (i < attr->range.getEnd())) - newFontAndColour.colour = *attr->getColour(); - } - - if (i > 0 && (newFontAndColour != lastFontAndColour || i == stringLength - 1)) - { - runAttributes.add (RunAttribute (lastFontAndColour, - Range (rangeStart, (i < stringLength - 1) ? i : (i + 1)))); - rangeStart = i; - } - - lastFontAndColour = newFontAndColour; - } - } - - for (int i = 0; i < runAttributes.size(); ++i) - { - const RunAttribute& r = runAttributes.getReference(i); - appendText (text, r.range, *(r.fontAndColour.font), r.fontAndColour.colour); - } - } - - OwnedArray tokens; - int totalLines; - - JUCE_DECLARE_NON_COPYABLE (TokenList); - }; -} - -//============================================================================== -void GlyphLayout::createStandardLayout (const AttributedString& text) -{ - GlyphLayoutHelpers::TokenList l; - l.createLayout (text, *this); -} - END_JUCE_NAMESPACE diff --git a/modules/juce_graphics/fonts/juce_AttributedString.h b/modules/juce_graphics/fonts/juce_AttributedString.h index 4e7b554cf2..63dc4c7bb8 100644 --- a/modules/juce_graphics/fonts/juce_AttributedString.h +++ b/modules/juce_graphics/fonts/juce_AttributedString.h @@ -34,6 +34,8 @@ An attributed string lets you create a string with varied fonts, colours, word-wrapping, layout, etc., and draw it using AttributedString::draw(). + + @see TextLayout */ class JUCE_API AttributedString { @@ -44,8 +46,12 @@ public: /** Creates an attributed string with the given text. */ explicit AttributedString (const String& text); - AttributedString (const AttributedString& other); - AttributedString& operator= (const AttributedString& other); + AttributedString (const AttributedString&); + AttributedString& operator= (const AttributedString&); + #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS + AttributedString (AttributedString&&) noexcept; + AttributedString& operator= (AttributedString&&) noexcept; + #endif /** Destructor. */ ~AttributedString(); @@ -54,12 +60,27 @@ public: /** Returns the complete text of this attributed string. */ const String& getText() const noexcept { return text; } - /** Sets the text. - This will change the text, but won't affect any of the attributes that have - been added. + /** Replaces all the text. + This will change the text, but won't affect any of the colour or font attributes + that have been added. */ void setText (const String& newText); + /** Appends some text (with a default font and colour). */ + void append (const String& textToAppend); + /** Appends some text, with a specified font, and the default colour (black). */ + void append (const String& textToAppend, const Font& font); + /** Appends some text, with a specified colour, and the default font. */ + void append (const String& textToAppend, const Colour& colour); + /** Appends some text, with a specified font and colour. */ + void append (const String& textToAppend, const Font& font, const Colour& colour); + + /** Resets the string, clearing all text and attributes. + Note that this won't affect global settings like the justification type, + word-wrap mode, etc. + */ + void clear(); + //============================================================================== /** Draws this string within the given area. The layout of the string within the rectangle is controlled by the justification @@ -143,7 +164,7 @@ public: /** If this attribute specifies a colour, this returns it; otherwise it returns nullptr. */ const Colour* getColour() const noexcept { return colour; } - /** The range of characters to which this attribute should be applied. */ + /** The range of characters to which this attribute will be applied. */ const Range range; private: @@ -165,9 +186,15 @@ public: /** Adds a colour attribute for the specified range. */ void setColour (const Range& range, const Colour& colour); + /** Removes all existing colour attributes, and applies this colour to the whole string. */ + void setColour (const Colour& colour); + /** Adds a font attribute for the specified range. */ void setFont (const Range& range, const Font& font); + /** Removes all existing font attributes, and applies this font to the whole string. */ + void setFont (const Font& font); + private: String text; float lineSpacing; @@ -177,142 +204,4 @@ private: OwnedArray attributes; }; - -//============================================================================== -/** - -*/ -class JUCE_API GlyphLayout -{ -public: - /** Creates an empty layout. */ - GlyphLayout(); - - /** Destructor. */ - ~GlyphLayout(); - - //============================================================================== - /** Creates a layout from the given attributed string. - This will replace any data that is currently stored in the layout. - */ - void setText (const AttributedString& text, float maxWidth); - - /** Draws the layout within the specified area. - The position of the text within the rectangle is controlled by the justification - flags set in the original AttributedString that was used to create this layout. - */ - void draw (Graphics& g, const Rectangle& area) const; - - //============================================================================== - /** A positioned glyph. */ - class JUCE_API Glyph - { - public: - Glyph (int glyphCode, const Point& anchor) noexcept; - ~Glyph(); - - const int glyphCode; - const Point anchor; - - private: - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Glyph); - }; - - //============================================================================== - /** A sequence of glyphs. */ - class JUCE_API Run - { - public: - Run(); - Run (const Range& range, int numGlyphsToPreallocate); - ~Run(); - - int getNumGlyphs() const noexcept { return glyphs.size(); } - const Font& getFont() const noexcept { return font; } - const Colour& getColour() const { return colour; } - Glyph& getGlyph (int index) const; - - void setStringRange (const Range& newStringRange) noexcept; - void setFont (const Font& newFont); - void setColour (const Colour& newColour) noexcept; - - void addGlyph (Glyph* glyph); - void ensureStorageAllocated (int numGlyphsNeeded); - - private: - OwnedArray glyphs; - Range stringRange; - Font font; - Colour colour; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Run); - }; - - //============================================================================== - /** A line containing a sequence of glyph-runs. */ - class JUCE_API Line - { - public: - Line() noexcept; - Line (const Range& stringRange, const Point& lineOrigin, - float ascent, float descent, float leading, int numRunsToPreallocate); - ~Line(); - - const Point& getLineOrigin() const noexcept { return lineOrigin; } - - float getAscent() const noexcept { return ascent; } - float getDescent() const noexcept { return descent; } - float getLeading() const noexcept { return leading; } - - int getNumRuns() const noexcept { return runs.size(); } - Run& getRun (int index) const noexcept; - - void setStringRange (const Range& newStringRange) noexcept; - void setLineOrigin (const Point& newLineOrigin) noexcept; - void setLeading (float newLeading) noexcept; - void increaseAscentDescent (float newAscent, float newDescent) noexcept; - - void addRun (Run* run); - - private: - OwnedArray runs; - Range stringRange; - Point lineOrigin; - float ascent, descent, leading; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Line); - }; - - //============================================================================== - /** Returns the maximum width of the content. */ - float getWidth() const noexcept { return width; } - - /** Returns the maximum height of the content. */ - float getHeight() const noexcept; - - /** Returns the number of lines in the layout. */ - int getNumLines() const noexcept { return lines.size(); } - - /** Returns one of the lines. */ - Line& getLine (int index) const; - - /** Adds a line to the layout. The object passed-in will be owned and deleted by the layout - when it is no longer needed. - */ - void addLine (Line* line); - - /** Pre-allocates space for the specified number of lines. */ - void ensureStorageAllocated (int numLinesNeeded); - -private: - OwnedArray lines; - float width; - Justification justification; - - void createStandardLayout (const AttributedString&); - bool createNativeLayout (const AttributedString&); - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GlyphLayout); -}; - #endif // __JUCE_ATTRIBUTEDSTRING_JUCEHEADER__ diff --git a/modules/juce_graphics/fonts/juce_TextLayout.cpp b/modules/juce_graphics/fonts/juce_TextLayout.cpp index f8242df035..e9724eeac6 100644 --- a/modules/juce_graphics/fonts/juce_TextLayout.cpp +++ b/modules/juce_graphics/fonts/juce_TextLayout.cpp @@ -25,351 +25,589 @@ BEGIN_JUCE_NAMESPACE +TextLayout::Glyph::Glyph (const int glyphCode_, const Point& anchor_, float width_) noexcept + : glyphCode (glyphCode_), anchor (anchor_), width (width_) +{ +} + +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 {} + //============================================================================== -class TextLayout::Token +TextLayout::Run::Run() noexcept + : colour (0xff000000) { -public: - Token (const String& t, - const Font& f, - const bool isWhitespace_) - : text (t), - font (f), - x(0), - y(0), - isWhitespace (isWhitespace_) - { - w = font.getStringWidth (t); - h = roundToInt (f.getHeight()); - isNewLine = t.containsChar ('\n') || t.containsChar ('\r'); - } +} - Token (const Token& other) - : text (other.text), - font (other.font), - x (other.x), - y (other.y), - w (other.w), - h (other.h), - line (other.line), - lineHeight (other.lineHeight), - isWhitespace (other.isWhitespace), - isNewLine (other.isNewLine) - { - } +TextLayout::Run::Run (const 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 {} - void draw (Graphics& g, - const int xOffset, - const int yOffset) +//============================================================================== +TextLayout::Line::Line() noexcept + : ascent (0.0f), descent (0.0f), leading (0.0f) +{ +} + +TextLayout::Line::Line (const Range& stringRange_, const Point& lineOrigin_, + const float ascent_, const float descent_, const float leading_, + const int numRunsToPreallocate) + : stringRange (stringRange_), lineOrigin (lineOrigin_), + ascent (ascent_), descent (descent_), leading (leading_) +{ + 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;) { - if (! isWhitespace) + const Run* run = runs.getUnchecked(i); + jassert (run != nullptr); + + if (run->glyphs.size() > 0) { - g.setFont (font); - g.drawSingleLineText (text.trimEnd(), - xOffset + x, - yOffset + y + (lineHeight - h) - + roundToInt (font.getAscent())); - } - } + float minX = run->glyphs.getReference(0).anchor.x; + float maxX = minX; - String text; - Font font; - int x, y, w, h; - int line, lineHeight; - bool isWhitespace, isNewLine; + 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); + } -private: - JUCE_LEAK_DETECTOR (Token); -}; + if (isFirst) + { + isFirst = false; + range = Range (minX, maxX); + } + else + { + range = range.getUnionWith (Range (minX, maxX)); + } + } + } + return range + lineOrigin.x; +} //============================================================================== TextLayout::TextLayout() - : totalLines (0) + : width (0), justification (Justification::topLeft) { - tokens.ensureStorageAllocated (64); } -TextLayout::TextLayout (const String& text, const Font& font) - : totalLines (0) +TextLayout::TextLayout (const TextLayout& other) + : width (other.width), + justification (other.justification) { - tokens.ensureStorageAllocated (64); - appendText (text, font); + lines.addCopiesOf (other.lines); } -TextLayout::TextLayout (const TextLayout& other) - : totalLines (0) +#if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS +TextLayout::TextLayout (TextLayout&& other) noexcept + : lines (static_cast &&> (other.lines)), + width (other.width), + justification (other.justification) { - *this = other; } -TextLayout& TextLayout::operator= (const TextLayout& other) +TextLayout& TextLayout::operator= (TextLayout&& other) noexcept { - if (this != &other) - { - clear(); - - totalLines = other.totalLines; - tokens.addCopiesOf (other.tokens); - } + lines = static_cast &&> (other.lines); + width = other.width; + justification = other.justification; + return *this; +} +#endif +TextLayout& TextLayout::operator= (const TextLayout& other) +{ + width = other.width; + justification = other.justification; + lines.clear(); + lines.addCopiesOf (other.lines); return *this; } TextLayout::~TextLayout() { - clear(); } -//============================================================================== -void TextLayout::clear() +float TextLayout::getHeight() const noexcept { - tokens.clear(); - totalLines = 0; + const Line* const lastLine = lines.getLast(); + + return lastLine != nullptr ? lastLine->lineOrigin.y + lastLine->descent + : 0; } -bool TextLayout::isEmpty() const +TextLayout::Line& TextLayout::getLine (const int index) const { - return tokens.size() == 0; + return *lines[index]; } -void TextLayout::appendText (const String& text, const Font& font) +void TextLayout::ensureStorageAllocated (int numLinesNeeded) { - String::CharPointerType t (text.getCharPointer()); - String currentString; - int lastCharType = 0; + lines.ensureStorageAllocated (numLinesNeeded); +} + +void TextLayout::addLine (Line* line) +{ + lines.add (line); +} - for (;;) +void TextLayout::draw (Graphics& g, const Rectangle& area) const +{ + const Point origin (justification.appliedToRectangle (Rectangle (0, 0, width, getHeight()), area).getPosition()); + + LowLevelGraphicsContext& context = *g.getInternalContext(); + + for (int i = 0; i < getNumLines(); ++i) { - const juce_wchar c = t.getAndAdvance(); - if (c == 0) - break; + const Line& line = getLine (i); + const Point lineOrigin (origin + line.lineOrigin); - int charType; - if (c == '\r' || c == '\n') + for (int j = 0; j < line.runs.size(); ++j) { - charType = 0; - } - else if (CharacterFunctions::isWhitespace (c)) - { - charType = 2; - } - else - { - charType = 1; - } + const Run* const run = line.runs.getUnchecked (j); + jassert (run != nullptr); + context.setFont (run->font); + context.setFill (run->colour); - if (charType == 0 || charType != lastCharType) - { - if (currentString.isNotEmpty()) + for (int k = 0; k < run->glyphs.size(); ++k) { - tokens.add (new Token (currentString, font, - lastCharType == 2 || lastCharType == 0)); + const Glyph& glyph = run->glyphs.getReference (k); + context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x, + lineOrigin.y + glyph.anchor.y)); } - - 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, lastCharType == 2)); } -void TextLayout::setText (const String& text, const Font& font) +void TextLayout::createLayout (const AttributedString& text, float maxWidth) { - clear(); - appendText (text, font); + lines.clear(); + width = maxWidth; + justification = text.getJustification(); + + if (! createNativeLayout (text)) + createStandardLayout (text); + + recalculateWidth(); } //============================================================================== -void TextLayout::layout (int maxWidth, - const Justification& justification, - const bool attemptToBalanceLineLengths) +namespace TextLayoutHelpers { - if (attemptToBalanceLineLengths) + struct FontAndColour + { + FontAndColour (const Font* font_) noexcept : font (font_), 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 { - const int originalW = maxWidth; - int bestWidth = maxWidth; - float bestLineProportion = 0.0f; + RunAttribute (const FontAndColour& fontAndColour_, const Range& range_) noexcept + : fontAndColour (fontAndColour_), range (range_) + {} - while (maxWidth > originalW / 2) + FontAndColour fontAndColour; + Range range; + }; + + struct Token + { + Token (const String& t, const Font& f, const Colour& c, const bool isWhitespace_) + : text (t), font (f), colour (c), + area (font.getStringWidth (t), roundToInt (f.getHeight())), + isWhitespace (isWhitespace_), + isNewLine (t.containsChar ('\n') || t.containsChar ('\r')) + {} + + const String text; + const Font font; + const Colour colour; + Rectangle area; + int line, lineHeight; + const bool isWhitespace, isNewLine; + + private: + Token& operator= (const Token&); + }; + + class TokenList + { + public: + TokenList() noexcept : totalLines (0) {} + + void createLayout (const AttributedString& text, TextLayout& layout) { - layout (maxWidth, justification, false); + tokens.ensureStorageAllocated (64); + layout.ensureStorageAllocated (totalLines); - if (getNumLines() <= 1) - return; + addTextRuns (text); - const int lastLineW = getLineWidth (getNumLines() - 1); - const int lastButOneLineW = getLineWidth (getNumLines() - 2); + layoutRuns ((int) layout.getWidth()); - const float prop = lastLineW / (float) lastButOneLineW; + int charPosition = 0; + int lineStartPosition = 0; + int runStartPosition = 0; - if (prop > 0.9f) - return; + TextLayout::Line* glyphLine = new TextLayout::Line(); + TextLayout::Run* glyphRun = new TextLayout::Run(); - if (prop > bestLineProportion) + for (int i = 0; i < tokens.size(); ++i) { - bestLineProportion = prop; - bestWidth = maxWidth; - } + const Token* const t = tokens.getUnchecked (i); + const Point tokenPos (t->area.getPosition().toFloat()); - maxWidth -= 10; - } + Array newGlyphs; + Array xOffsets; + t->font.getGlyphPositions (t->text.trimEnd(), newGlyphs, xOffsets); - layout (bestWidth, justification, false); - } - else - { - int x = 0; - int y = 0; - int h = 0; - totalLines = 0; - int i; + glyphRun->glyphs.ensureStorageAllocated (glyphRun->glyphs.size() + newGlyphs.size()); - for (i = 0; i < tokens.size(); ++i) - { - Token* const t = tokens.getUnchecked(i); - t->x = x; - t->y = y; - t->line = totalLines; - x += t->w; - h = jmax (h, t->h); + for (int j = 0; j < newGlyphs.size(); ++j) + { + if (charPosition == lineStartPosition) + glyphLine->lineOrigin = tokenPos.translated (0, t->font.getAscent()); + + const float x = xOffsets.getUnchecked (j); + glyphRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked(j), + Point (tokenPos.getX() + x, 0), + xOffsets.getUnchecked (j + 1) - x)); + ++charPosition; + } - const Token* nextTok = tokens [i + 1]; + if (t->isWhitespace || t->isNewLine) + ++charPosition; - if (nextTok == 0) - break; + const Token* const nextToken = tokens [i + 1]; - if (t->isNewLine || ((! nextTok->isWhitespace) && x + nextTok->w > maxWidth)) + if (nextToken == nullptr) // this is the last token + { + addRun (glyphLine, glyphRun, t, runStartPosition, charPosition); + glyphLine->stringRange = Range (lineStartPosition, charPosition); + layout.addLine (glyphLine); + } + else + { + if (t->font != nextToken->font || t->colour != nextToken->colour) + { + addRun (glyphLine, glyphRun, t, runStartPosition, charPosition); + runStartPosition = charPosition; + glyphRun = new TextLayout::Run(); + } + + if (t->line != nextToken->line) + { + addRun (glyphLine, glyphRun, t, runStartPosition, charPosition); + glyphLine->stringRange = Range (lineStartPosition, charPosition); + layout.addLine (glyphLine); + + runStartPosition = charPosition; + lineStartPosition = charPosition; + glyphLine = new TextLayout::Line(); + glyphRun = new TextLayout::Run(); + } + } + } + + if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0) { - // finished a line, so go back and update the heights of the things on it - for (int j = i; j >= 0; --j) + const int totalW = (int) layout.getWidth(); + + for (int i = 0; i < totalLines; ++i) { - Token* const tok = tokens.getUnchecked(j); + const int lineW = getLineWidth (i); + float dx = 0; - if (tok->line == totalLines) - tok->lineHeight = h; + if ((text.getJustification().getFlags() & Justification::right) != 0) + dx = (float) (totalW - lineW); else - break; - } + dx = (totalW - lineW) / 2.0f; - x = 0; - y += h; - h = 0; - ++totalLines; + TextLayout::Line& glyphLine = layout.getLine (i); + glyphLine.lineOrigin.x += dx; + } } } - // finished a line, so go back and update the heights of the things on it - for (int j = jmin (i, tokens.size() - 1); j >= 0; --j) + private: + static void addRun (TextLayout::Line* glyphLine, TextLayout::Run* glyphRun, + const Token* const t, const int start, const int end) { - Token* const t = tokens.getUnchecked(j); - - if (t->line == totalLines) - t->lineHeight = h; - else - break; + 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); } - ++totalLines; - - if (! justification.testFlags (Justification::left)) + void appendText (const AttributedString& text, const Range& stringRange, + const Font& font, const Colour& colour) { - int totalW = getWidth(); + String stringText (text.getText().substring (stringRange.getStart(), stringRange.getEnd())); + String::CharPointerType t (stringText.getCharPointer()); + String currentString; + int lastCharType = 0; - for (i = totalLines; --i >= 0;) + for (;;) { - const int lineW = getLineWidth (i); + const juce_wchar c = t.getAndAdvance(); + if (c == 0) + break; + + int charType; + if (c == '\r' || c == '\n') + charType = 0; + else if (CharacterFunctions::isWhitespace (c)) + charType = 2; + else + charType = 1; + + if (charType == 0 || charType != lastCharType) + { + if (currentString.isNotEmpty()) + tokens.add (new Token (currentString, font, colour, + lastCharType == 2 || lastCharType == 0)); - int dx = 0; - if (justification.testFlags (Justification::horizontallyCentred)) - dx = (totalW - lineW) / 2; - else if (justification.testFlags (Justification::right)) - dx = totalW - lineW; + currentString = String::charToString (c); - for (int j = tokens.size(); --j >= 0;) + if (c == '\r' && *t == '\n') + currentString += t.getAndAdvance(); + } + else { - Token* const t = tokens.getUnchecked(j); + currentString += c; + } + + lastCharType = charType; + } + + if (currentString.isNotEmpty()) + tokens.add (new Token (currentString, font, colour, lastCharType == 2)); + } + + void layoutRuns (const int maxWidth) + { + int x = 0, y = 0, h = 0; + int i; + + for (i = 0; i < tokens.size(); ++i) + { + Token* const t = tokens.getUnchecked(i); + t->area.setPosition (x, y); + t->line = totalLines; + x += t->area.getWidth(); + h = jmax (h, t->area.getHeight()); - if (t->line == i) - t->x += dx; + const Token* nextTok = tokens[i + 1]; + + if (nextTok == 0) + 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; } - } -} -//============================================================================== -int TextLayout::getLineWidth (const int lineNumber) const -{ - int maxW = 0; + void setLastLineHeight (int i, const int height) noexcept + { + while (--i >= 0) + { + Token* const tok = tokens.getUnchecked (i); - for (int i = tokens.size(); --i >= 0;) - { - const Token* const t = tokens.getUnchecked(i); + if (tok->line == totalLines) + tok->lineHeight = height; + else + break; + } + } - if (t->line == lineNumber && ! t->isWhitespace) - maxW = jmax (maxW, t->x + t->w); - } + int getLineWidth (const int lineNumber) const noexcept + { + int maxW = 0; - return maxW; -} + for (int i = tokens.size(); --i >= 0;) + { + const Token* const t = tokens.getUnchecked (i); -int TextLayout::getWidth() const -{ - int maxW = 0; + if (t->line == lineNumber && ! t->isWhitespace) + maxW = jmax (maxW, t->area.getRight()); + } - for (int i = tokens.size(); --i >= 0;) - { - const Token* const t = tokens.getUnchecked(i); - if (! t->isWhitespace) - maxW = jmax (maxW, t->x + t->w); - } + return maxW; + } - return maxW; + void addTextRuns (const AttributedString& text) + { + Font defaultFont; + Array runAttributes; + + { + const int stringLength = text.getText().length(); + int rangeStart = 0; + FontAndColour lastFontAndColour (nullptr); + + // 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* const attr = text.getAttribute (j); + + // Check if the current character falls within the range of a font attribute + if (attr->getFont() != nullptr && (i >= attr->range.getStart()) && (i < attr->range.getEnd())) + newFontAndColour.font = attr->getFont(); + + // Check if the current character falls within the range of a foreground colour attribute + if (attr->getColour() != nullptr && (i >= attr->range.getStart()) && (i < attr->range.getEnd())) + newFontAndColour.colour = *attr->getColour(); + } + + if (i > 0 && (newFontAndColour != lastFontAndColour || i == stringLength - 1)) + { + runAttributes.add (RunAttribute (lastFontAndColour, + Range (rangeStart, (i < stringLength - 1) ? i : (i + 1)))); + rangeStart = i; + } + + lastFontAndColour = newFontAndColour; + } + } + + for (int i = 0; i < runAttributes.size(); ++i) + { + const RunAttribute& r = runAttributes.getReference(i); + appendText (text, r.range, *(r.fontAndColour.font), r.fontAndColour.colour); + } + } + + OwnedArray tokens; + int totalLines; + + JUCE_DECLARE_NON_COPYABLE (TokenList); + }; } -int TextLayout::getHeight() const +//============================================================================== +void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth) { - int maxH = 0; + const float minimumWidth = maxWidth / 2.0; + float bestWidth = maxWidth; + float bestLineProportion = 0.0f; - for (int i = tokens.size(); --i >= 0;) + while (maxWidth > minimumWidth) { - const Token* const t = tokens.getUnchecked(i); + createLayout (text, maxWidth); + + 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 prop = jmax (line1, line2) / jmin (line1, line2); - if (! t->isWhitespace) - maxH = jmax (maxH, t->y + t->h); + if (prop > 0.9f) + return; + + if (prop > bestLineProportion) + { + bestLineProportion = prop; + bestWidth = maxWidth; + } + + maxWidth -= 10.0f; } - return maxH; + if (bestWidth != maxWidth) + createLayout (text, bestWidth); } //============================================================================== -void TextLayout::draw (Graphics& g, - const int xOffset, - const int yOffset) const +void TextLayout::createStandardLayout (const AttributedString& text) { - for (int i = tokens.size(); --i >= 0;) - tokens.getUnchecked(i)->draw (g, xOffset, yOffset); + TextLayoutHelpers::TokenList l; + l.createLayout (text, *this); } -void TextLayout::drawWithin (Graphics& g, - int x, int y, int w, int h, - const Justification& justification) const +void TextLayout::recalculateWidth() { - justification.applyToRectangle (x, y, getWidth(), getHeight(), - x, y, w, h); + if (lines.size() > 0) + { + Range range (lines.getFirst()->getLineBoundsX()); - draw (g, x, y); -} + int i; + for (i = lines.size(); --i > 0;) + range = range.getUnionWith (lines.getUnchecked(i)->getLineBoundsX()); + + for (i = lines.size(); --i >= 0;) + lines.getUnchecked(i)->lineOrigin.x -= range.getStart(); + width = range.getLength(); + } +} END_JUCE_NAMESPACE diff --git a/modules/juce_graphics/fonts/juce_TextLayout.h b/modules/juce_graphics/fonts/juce_TextLayout.h index 6378075299..a9dea4335a 100644 --- a/modules/juce_graphics/fonts/juce_TextLayout.h +++ b/modules/juce_graphics/fonts/juce_TextLayout.h @@ -30,124 +30,145 @@ #include "../placement/juce_Justification.h" class Graphics; - //============================================================================== /** - A laid-out arrangement of text. - - You can add text in different fonts to a TextLayout object, then call its - layout() method to word-wrap it into lines. The layout can then be drawn - using a graphics context. + A Pre-formatted piece of text, which may contain multiple fonts and colours. - It's handy if you've got a message to display, because you can format it, - measure the extent of the layout, and then create a suitably-sized window - to show it in. + A TextLayout is created from an AttributedString, and once created can be + quickly drawn into a Graphics context. - @see Font, Graphics::drawFittedText, GlyphArrangement + @see AttributedString */ class JUCE_API TextLayout { public: - //============================================================================== - /** Creates an empty text layout. - - Text can then be appended using the appendText() method. + /** Creates an empty layout. + Having created a TextLayout, you can populate it using createLayout() or + createLayoutWithBalancedLineLengths(). */ TextLayout(); - - /** Creates a copy of another layout object. */ - TextLayout (const TextLayout& other); - - /** Creates a text layout from an initial string and font. */ - TextLayout (const String& text, const Font& font); + TextLayout (const TextLayout&); + TextLayout& operator= (const TextLayout&); + #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS + TextLayout (TextLayout&& other) noexcept; + TextLayout& operator= (TextLayout&&) noexcept; + #endif /** Destructor. */ ~TextLayout(); - /** Copies another layout onto this one. */ - TextLayout& operator= (const TextLayout& layoutToCopy); - //============================================================================== - /** Clears the layout, removing all its text. */ - void clear(); - - /** Adds a string to the end of the arrangement. - - The string will be broken onto new lines wherever it contains - carriage-returns or linefeeds. After adding it, you can call layout() - to wrap long lines into a paragraph and justify it. + /** Creates a layout from the given attributed string. + This will replace any data that is currently stored in the layout. */ - void appendText (const String& textToAppend, - const Font& fontToUse); + void createLayout (const AttributedString& text, float maxWidth); - /** Replaces all the text with a new string. + /** Creates a layout, attempting to choose a width which results in lines + of a similar length. - This is equivalent to calling clear() followed by appendText(). + This will be slower than the normal createLayout method, but produces a + tidier result. */ - void setText (const String& newText, - const Font& fontToUse); + void createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth); - /** Returns true if the layout has not had any text added yet. */ - bool isEmpty() const; - - //============================================================================== - /** Breaks the text up to form a paragraph with the given width. - - @param maximumWidth any text wider than this will be split - across multiple lines - @param justification how the lines are to be laid-out horizontally - @param attemptToBalanceLineLengths if true, it will try to split the lines at a - width that keeps all the lines of text at a - similar length - this is good when you're displaying - a short message and don't want it to get split - onto two lines with only a couple of words on - the second line, which looks untidy. + /** Draws the layout within the specified area. + The position of the text within the rectangle is controlled by the justification + flags set in the original AttributedString that was used to create this layout. */ - void layout (int maximumWidth, - const Justification& justification, - bool attemptToBalanceLineLengths); - + void draw (Graphics& g, const Rectangle& area) const; //============================================================================== - /** Returns the overall width of the entire text layout. */ - int getWidth() const; + /** A positioned glyph. */ + class JUCE_API Glyph + { + public: + Glyph (int glyphCode, const Point& anchor, float width) noexcept; + Glyph (const Glyph&) noexcept; + Glyph& operator= (const Glyph&) noexcept; + ~Glyph() noexcept; + + /** The code number of this glyph. */ + int glyphCode; + + /** The glyph's anchor point - this is relative to the line's origin. + @see TextLayout::Line::lineOrigin + */ + Point anchor; + + float width; + }; - /** Returns the overall height of the entire text layout. */ - int getHeight() const; + //============================================================================== + /** A sequence of glyphs with a common font and colour. */ + class JUCE_API Run + { + public: + Run() noexcept; + Run (const Run&); + Run (const Range& stringRange, int numGlyphsToPreallocate); + ~Run() noexcept; + + Font font; /**< The run's font. */ + Colour colour; /**< The run's colour. */ + Array glyphs; /**< The glyphs in this run. */ + Range stringRange; /**< The character range that this run represents in the + original string that was used to create it. */ + private: + Run& operator= (const Run&); + }; - /** Returns the total number of lines of text. */ - int getNumLines() const { return totalLines; } + //============================================================================== + /** A line containing a sequence of glyph-runs. */ + class JUCE_API Line + { + public: + Line() noexcept; + Line (const Line&); + Line (const Range& stringRange, const Point& lineOrigin, + float ascent, float descent, float leading, int numRunsToPreallocate); + ~Line() noexcept; + + /** Returns the X position range which contains all the glyphs in this line. */ + Range getLineBoundsX() const noexcept; + + OwnedArray runs; /**< The glyph-runs in this line. */ + Range stringRange; /**< The character range that this line represents in the + original string that was used to create it. */ + Point lineOrigin; /**< The line's baseline origin. */ + float ascent, descent, leading; + + private: + Line& operator= (const Line&); + }; - /** Returns the width of a particular line of text. + //============================================================================== + /** Returns the maximum width of the content. */ + float getWidth() const noexcept { return width; } - @param lineNumber the line, from 0 to (getNumLines() - 1) - */ - int getLineWidth (int lineNumber) const; + /** Returns the maximum height of the content. */ + float getHeight() const noexcept; - //============================================================================== - /** Renders the text at a specified position using a graphics context. - */ - void draw (Graphics& g, int topLeftX, int topLeftY) const; + /** Returns the number of lines in the layout. */ + int getNumLines() const noexcept { return lines.size(); } - /** Renders the text within a specified rectangle using a graphics context. + /** Returns one of the lines. */ + Line& getLine (int index) const; - The justification flags dictate how the block of text should be positioned - within the rectangle. - */ - void drawWithin (Graphics& g, - int x, int y, int w, int h, - const Justification& layoutFlags) const; + /** Adds a line to the layout. The layout will take ownership of this line object + and will delete it when it is no longer needed. */ + void addLine (Line* line); + /** Pre-allocates space for the specified number of lines. */ + void ensureStorageAllocated (int numLinesNeeded); private: - //============================================================================== - class Token; - friend class OwnedArray ; - OwnedArray tokens; - int totalLines; + OwnedArray lines; + float width; + Justification justification; - JUCE_LEAK_DETECTOR (TextLayout); + void createStandardLayout (const AttributedString&); + bool createNativeLayout (const AttributedString&); + void recalculateWidth(); }; - #endif // __JUCE_TEXTLAYOUT_JUCEHEADER__ diff --git a/modules/juce_graphics/juce_graphics.h b/modules/juce_graphics/juce_graphics.h index 7d354fc9f6..8cbbcb96de 100644 --- a/modules/juce_graphics/juce_graphics.h +++ b/modules/juce_graphics/juce_graphics.h @@ -46,7 +46,7 @@ management and layout. */ #ifndef JUCE_USE_DIRECTWRITE - #define JUCE_USE_DIRECTWRITE 0 + #define JUCE_USE_DIRECTWRITE 1 #endif #ifndef JUCE_INCLUDE_PNGLIB_CODE diff --git a/modules/juce_graphics/native/juce_mac_Fonts.mm b/modules/juce_graphics/native/juce_mac_Fonts.mm index bdb4c86878..1495e25914 100644 --- a/modules/juce_graphics/native/juce_mac_Fonts.mm +++ b/modules/juce_graphics/native/juce_mac_Fonts.mm @@ -33,6 +33,116 @@ namespace CoreTextTypeLayout { + CTFontRef createCTFont (const Font& font, float fontSize, bool& needsItalicTransform) + { + CFStringRef cfName = font.getTypefaceName().toCFString(); + CTFontRef ctFontRef = CTFontCreateWithName (cfName, fontSize, nullptr); + CFRelease (cfName); + + if (ctFontRef != nullptr) + { + if (font.isItalic()) + { + CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr, + kCTFontItalicTrait, kCTFontItalicTrait); + + if (newFont != nullptr) + { + CFRelease (ctFontRef); + ctFontRef = newFont; + } + else + { + needsItalicTransform = true; // couldn't find a proper italic version, so fake it with a transform.. + } + } + + if (font.isBold()) + { + CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr, + kCTFontBoldTrait, kCTFontBoldTrait); + if (newFont != nullptr) + { + CFRelease (ctFontRef); + ctFontRef = newFont; + } + } + } + + return ctFontRef; + } + + float getFontHeightToCGSizeFactor (const Font& font) + { + static float factor = 0; + + if (factor == 0) // (This factor seems to be a constant for all fonts..) + { + bool needsItalicTransform = false; + CTFontRef tempFont = createCTFont (font, 1024, needsItalicTransform); + CGFontRef cgFontRef = CTFontCopyGraphicsFont (tempFont, nullptr); + const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef)); + factor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight; + CGFontRelease (cgFontRef); + CFRelease (tempFont); + } + + return factor; + } + + //============================================================================== + struct Advances + { + Advances (CTRunRef run, const CFIndex numGlyphs) + : advances (CTRunGetAdvancesPtr (run)) + { + if (advances == nullptr) + { + local.malloc (numGlyphs); + CTRunGetAdvances (run, CFRangeMake (0, 0), local); + advances = local; + } + } + + const CGSize* advances; + HeapBlock local; + }; + + struct Glyphs + { + Glyphs (CTRunRef run, const int numGlyphs) + : glyphs (CTRunGetGlyphsPtr (run)) + { + if (glyphs == nullptr) + { + local.malloc (numGlyphs); + CTRunGetGlyphs (run, CFRangeMake (0, 0), local); + glyphs = local; + } + } + + const CGGlyph* glyphs; + HeapBlock local; + }; + + struct Positions + { + Positions (CTRunRef run, const int numGlyphs) + : points (CTRunGetPositionsPtr (run)) + { + if (points == nullptr) + { + local.malloc (numGlyphs); + CTRunGetPositions (run, CFRangeMake (0, 0), local); + points = local; + } + } + + const CGPoint* points; + HeapBlock local; + }; + + //============================================================================== CFAttributedStringRef createCFAttributedString (const AttributedString& text) { #if JUCE_IOS @@ -58,17 +168,10 @@ namespace CoreTextTypeLayout if (attr->getFont() != nullptr) { - // Apply fontHeightToCGSizeFactor to the font size since this is how glyphs are drawn - CTFontRef ctFontRef = CTFontCreateWithName (attr->getFont()->getTypefaceName().toCFString(), 1024, nullptr); - CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr); - CFRelease (ctFontRef); - - const int totalHeight = abs (CGFontGetAscent (cgFontRef)) + abs (CGFontGetDescent (cgFontRef)); - float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight; - CGFontRelease (cgFontRef); - - ctFontRef = CTFontCreateWithName (attr->getFont()->getTypefaceName().toCFString(), - attr->getFont()->getHeight() * fontHeightToCGSizeFactor, nullptr); + const Font& f = *attr->getFont(); + bool needsItalicTransform = false; + CTFontRef ctFontRef = createCTFont (f, f.getHeight() * getFontHeightToCGSizeFactor (f), + needsItalicTransform); CFAttributedStringSetAttribute (attribString, CFRangeMake (range.getStart(), range.getLength()), kCTFontAttributeName, ctFontRef); @@ -190,7 +293,7 @@ namespace CoreTextTypeLayout CFRelease (frame); } - void createLayout (GlyphLayout& glyphLayout, const AttributedString& text) + void createLayout (TextLayout& glyphLayout, const AttributedString& text) { CFAttributedStringRef attribString = CoreTextTypeLayout::createCFAttributedString (text); CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString (attribString); @@ -228,9 +331,9 @@ namespace CoreTextTypeLayout CGFloat ascent, descent, leading; CTLineGetTypographicBounds (line, &ascent, &descent, &leading); - GlyphLayout::Line* const glyphLine = new GlyphLayout::Line (lineStringRange, lineOrigin, - (float) ascent, (float) descent, (float) leading, - (int) numRuns); + TextLayout::Line* const glyphLine = new TextLayout::Line (lineStringRange, lineOrigin, + (float) ascent, (float) descent, (float) leading, + (int) numRuns); glyphLayout.addLine (glyphLine); for (CFIndex j = 0; j < numRuns; ++j) @@ -239,10 +342,10 @@ namespace CoreTextTypeLayout const CFIndex numGlyphs = CTRunGetGlyphCount (run); const CFRange runStringRange = CTRunGetStringRange (run); - GlyphLayout::Run* const glyphRun = new GlyphLayout::Run (Range ((int) runStringRange.location, - (int) (runStringRange.location + runStringRange.length - 1)), - (int) numGlyphs); - glyphLine->addRun (glyphRun); + TextLayout::Run* const glyphRun = new TextLayout::Run (Range ((int) runStringRange.location, + (int) (runStringRange.location + runStringRange.length - 1)), + (int) numGlyphs); + glyphLine->runs.add (glyphRun); CFDictionaryRef runAttributes = CTRunGetAttributes (run); @@ -250,18 +353,16 @@ namespace CoreTextTypeLayout if (CFDictionaryGetValueIfPresent (runAttributes, kCTFontAttributeName, (const void **) &ctRunFont)) { CFStringRef cfsFontName = CTFontCopyPostScriptName (ctRunFont); - const String fontName (String::fromCFString (cfsFontName)); - CTFontRef ctFontRef = CTFontCreateWithName (cfsFontName, 1024, nullptr); - CFRelease (cfsFontName); CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr); CFRelease (ctFontRef); - const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef)); const float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight; CGFontRelease (cgFontRef); - glyphRun->setFont (Font (fontName, CTFontGetSize (ctRunFont) / fontHeightToCGSizeFactor, 0)); + glyphRun->font = Font (String::fromCFString (cfsFontName), + CTFontGetSize (ctRunFont) / fontHeightToCGSizeFactor, 0); // XXX bold/italic flags? + CFRelease (cfsFontName); } CGColorRef cgRunColor; @@ -270,28 +371,17 @@ namespace CoreTextTypeLayout { const CGFloat* const components = CGColorGetComponents (cgRunColor); - glyphRun->setColour (Colour::fromFloatRGBA (components[0], components[1], components[2], components[3])); + glyphRun->colour = Colour::fromFloatRGBA (components[0], components[1], components[2], components[3]); } - const CGGlyph* glyphsPtr = CTRunGetGlyphsPtr (run); - const CGPoint* posPtr = CTRunGetPositionsPtr (run); - HeapBlock glyphBuffer; - HeapBlock positionBuffer; - - if (glyphsPtr == nullptr || posPtr == nullptr) - { - // If we can't get a direct pointer, we have to copy the metrics to get them.. - glyphBuffer.malloc (numGlyphs); - glyphsPtr = glyphBuffer; - CTRunGetGlyphs (run, CFRangeMake (0, 0), glyphBuffer); - - positionBuffer.malloc (numGlyphs); - posPtr = positionBuffer; - CTRunGetPositions (run, CFRangeMake (0, 0), positionBuffer); - } + const CoreTextTypeLayout::Glyphs glyphs (run, numGlyphs); + const CoreTextTypeLayout::Advances advances (run, numGlyphs); + const CoreTextTypeLayout::Positions positions (run, numGlyphs); for (CFIndex k = 0; k < numGlyphs; ++k) - glyphRun->addGlyph (new GlyphLayout::Glyph (glyphsPtr[k], Point (posPtr[k].x, posPtr[k].y))); + glyphRun->glyphs.add (TextLayout::Glyph (glyphs.glyphs[k], Point (positions.points[k].x, + positions.points[k].y), + advances.advances[k].width)); } } @@ -314,41 +404,11 @@ public: ascent (0.0f), unitsToHeightScaleFactor (0.0f) { - CFStringRef cfName = font.getTypefaceName().toCFString(); - ctFontRef = CTFontCreateWithName (cfName, 1024, nullptr); - CFRelease (cfName); + bool needsItalicTransform = false; + ctFontRef = CoreTextTypeLayout::createCTFont (font, 1024.0f, needsItalicTransform); if (ctFontRef != nullptr) { - bool needsItalicTransform = false; - - if (font.isItalic()) - { - CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr, - kCTFontItalicTrait, kCTFontItalicTrait); - - if (newFont != nullptr) - { - CFRelease (ctFontRef); - ctFontRef = newFont; - } - else - { - needsItalicTransform = true; // couldn't find a proper italic version, so fake it with a transform.. - } - } - - if (font.isBold()) - { - CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr, - kCTFontBoldTrait, kCTFontBoldTrait); - if (newFont != nullptr) - { - CFRelease (ctFontRef); - ctFontRef = newFont; - } - } - ascent = std::abs ((float) CTFontGetAscent (ctFontRef)); const float totalSize = ascent + std::abs ((float) CTFontGetDescent (ctFontRef)); ascent /= totalSize; @@ -411,11 +471,11 @@ public: { CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i); CFIndex length = CTRunGetGlyphCount (run); - HeapBlock advances (length); - CTRunGetAdvances (run, CFRangeMake (0, 0), advances); + + const CoreTextTypeLayout::Advances advances (run, length); for (int j = 0; j < length; ++j) - x += (float) advances[j].width; + x += (float) advances.advances[j].width; } CFRelease (line); @@ -446,16 +506,15 @@ public: { CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i); CFIndex length = CTRunGetGlyphCount (run); - HeapBlock advances (length); - CTRunGetAdvances (run, CFRangeMake (0, 0), advances); - HeapBlock glyphs (length); - CTRunGetGlyphs (run, CFRangeMake (0, 0), glyphs); + + const CoreTextTypeLayout::Advances advances (run, length); + const CoreTextTypeLayout::Glyphs glyphs (run, length); for (int j = 0; j < length; ++j) { - x += (float) advances[j].width; + x += (float) advances.advances[j].width; xOffsets.add (x * unitsToHeightScaleFactor); - resultGlyphs.add (glyphs[j]); + resultGlyphs.add (glyphs.glyphs[j]); } } @@ -1048,7 +1107,7 @@ Typeface::Ptr Font::getDefaultTypefaceForFont (const Font& font) return Typeface::createSystemTypefaceFor (f); } -bool GlyphLayout::createNativeLayout (const AttributedString& text) +bool TextLayout::createNativeLayout (const AttributedString& text) { #if JUCE_CORETEXT_AVAILABLE CoreTextTypeLayout::createLayout (*this, text); diff --git a/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp b/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp index 43464d54a7..0b9ca607d9 100644 --- a/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp +++ b/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp @@ -65,7 +65,7 @@ namespace DirectWriteTypeLayout DWRITE_GLYPH_RUN const* glyphRun, DWRITE_GLYPH_RUN_DESCRIPTION const* runDescription, IUnknown* clientDrawingEffect) { - GlyphLayout* const glyphLayout = static_cast (clientDrawingContext); + TextLayout* const layout = static_cast (clientDrawingContext); if (baselineOriginY != lastOriginY) { @@ -73,49 +73,52 @@ namespace DirectWriteTypeLayout ++currentLine; // The x value is only correct when dealing with LTR text - glyphLayout->getLine (currentLine).setLineOrigin (Point (baselineOriginX, baselineOriginY)); + layout->getLine (currentLine).lineOrigin = Point (baselineOriginX, baselineOriginY); } if (currentLine < 0) return S_OK; - GlyphLayout::Line& glyphLine = glyphLayout->getLine (currentLine); + TextLayout::Line& glyphLine = layout->getLine (currentLine); DWRITE_FONT_METRICS dwFontMetrics; glyphRun->fontFace->GetMetrics (&dwFontMetrics); - const float ascent = scaledFontSize (dwFontMetrics.ascent, dwFontMetrics, glyphRun); - const float descent = scaledFontSize (dwFontMetrics.descent, dwFontMetrics, glyphRun); - glyphLine.increaseAscentDescent (ascent, descent); + glyphLine.ascent = jmax (glyphLine->ascent, scaledFontSize (dwFontMetrics.ascent, dwFontMetrics, glyphRun)); + glyphLine.descent = jmax (glyphLine->descent, scaledFontSize (dwFontMetrics.descent, dwFontMetrics, glyphRun)); int styleFlags = 0; const String fontName (getFontName (glyphRun, styleFlags)); - GlyphLayout::Run* const glyphRunLayout = new GlyphLayout::Run (Range (runDescription->textPosition, - runDescription->textPosition + runDescription->stringLength), - glyphRun->glyphCount); - glyphLine.addRun (glyphRunLayout); + TextLayout::Run* const glyphRunLayout = new TextLayout::Run (Range (runDescription->textPosition, + runDescription->textPosition + runDescription->stringLength), + glyphRun->glyphCount); + glyphLine.runs.add (glyphRunLayout); glyphRun->fontFace->GetMetrics (&dwFontMetrics); const float totalHeight = std::abs ((float) dwFontMetrics.ascent) + std::abs ((float) dwFontMetrics.descent); const float fontHeightToEmSizeFactor = (float) dwFontMetrics.designUnitsPerEm / totalHeight; - glyphRunLayout->setFont (Font (fontName, glyphRun->fontEmSize / fontHeightToEmSizeFactor, styleFlags)); - glyphRunLayout->setColour (getColourOf (static_cast (clientDrawingEffect))); + glyphRunLayout->font = Font (fontName, glyphRun->fontEmSize / fontHeightToEmSizeFactor, styleFlags); + glyphRunLayout->colour = getColourOf (static_cast (clientDrawingEffect)); - const Point lineOrigin (glyphLayout->getLine (currentLine).getLineOrigin()); + const Point lineOrigin (layout->getLine (currentLine).lineOrigin); float x = baselineOriginX - lineOrigin.x; for (UINT32 i = 0; i < glyphRun->glyphCount; ++i) { + const float advance = glyphRun->glyphAdvances[i]; + if ((glyphRun->bidiLevel & 1) != 0) - x -= glyphRun->glyphAdvances[i]; // RTL text + x -= advance; // RTL text - glyphRunLayout->addGlyph (new GlyphLayout::Glyph (glyphRun->glyphIndices[i], Point (x, baselineOriginY - lineOrigin.y))); + glyphRunLayout->glyphs.add (TextLayout::Glyph (glyphRun->glyphIndices[i], + Point (x, baselineOriginY - lineOrigin.y), + advance)); if ((glyphRun->bidiLevel & 1) == 0) - x += glyphRun->glyphAdvances[i]; // LTR text + x += advance; // LTR text } return S_OK; @@ -264,7 +267,7 @@ namespace DirectWriteTypeLayout } } - void createLayout (GlyphLayout& glyphLayout, const AttributedString& text, IDWriteFactory* const directWriteFactory, + void createLayout (TextLayout& layout, const AttributedString& text, IDWriteFactory* const directWriteFactory, ID2D1Factory* const direct2dFactory, IDWriteFontCollection* const fontCollection) { // To add color to text, we need to create a D2D render target @@ -296,7 +299,7 @@ namespace DirectWriteTypeLayout ComSmartPtr dwTextLayout; hr = directWriteFactory->CreateTextLayout (text.getText().toWideCharPointer(), textLen, - dwTextFormat, glyphLayout.getWidth(), + dwTextFormat, layout.getWidth(), 1.0e7f, dwTextLayout.resetAndGetPointerAddress()); const int numAttributes = text.getNumAttributes(); @@ -307,7 +310,7 @@ namespace DirectWriteTypeLayout UINT32 actualLineCount = 0; hr = dwTextLayout->GetLineMetrics (nullptr, 0, &actualLineCount); - glyphLayout.ensureStorageAllocated (actualLineCount); + layout.ensureStorageAllocated (actualLineCount); HeapBlock dwLineMetrics (actualLineCount); hr = dwTextLayout->GetLineMetrics (dwLineMetrics, actualLineCount, &actualLineCount); @@ -318,19 +321,19 @@ namespace DirectWriteTypeLayout const Range lineStringRange (lastLocation, (int) lastLocation + dwLineMetrics[i].length); lastLocation = dwLineMetrics[i].length; - GlyphLayout::Line* glyphLine = new GlyphLayout::Line(); - glyphLayout.addLine (glyphLine); - glyphLine->setStringRange (lineStringRange); + TextLayout::Line* glyphLine = new TextLayout::Line(); + layout.addLine (glyphLine); + glyphLine->stringRange = lineStringRange; } ComSmartPtr textRenderer (new CustomDirectWriteTextRenderer (fontCollection)); - hr = dwTextLayout->Draw (&glyphLayout, textRenderer, 0, 0); + hr = dwTextLayout->Draw (&layout, textRenderer, 0, 0); } } #endif -bool GlyphLayout::createNativeLayout (const AttributedString& text) +bool TextLayout::createNativeLayout (const AttributedString& text) { #if JUCE_USE_DIRECTWRITE const Direct2DFactories& factories = Direct2DFactories::getInstance(); diff --git a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.cpp b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.cpp index 2be87be6e8..1c4f4bab7d 100644 --- a/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.cpp +++ b/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.cpp @@ -80,10 +80,10 @@ namespace LookAndFeelHelpers p.closeSubPath(); } - const Colour createBaseColour (const Colour& buttonColour, - const bool hasKeyboardFocus, - const bool isMouseOverButton, - const bool isButtonDown) noexcept + Colour createBaseColour (const Colour& buttonColour, + const bool hasKeyboardFocus, + const bool isMouseOverButton, + const bool isButtonDown) noexcept { const float sat = hasKeyboardFocus ? 1.3f : 0.9f; const Colour baseColour (buttonColour.withMultipliedSaturation (sat)); @@ -96,15 +96,17 @@ namespace LookAndFeelHelpers return baseColour; } - const TextLayout layoutTooltipText (const String& text) noexcept + TextLayout layoutTooltipText (const String& text) noexcept { - const float tooltipFontSize = 12.0f; + const float tooltipFontSize = 13.0f; const int maxToolTipWidth = 400; - const Font f (tooltipFontSize, Font::bold); - TextLayout tl (text, f); - tl.layout (maxToolTipWidth, Justification::left, true); + AttributedString s; + s.setJustification (Justification::centred); + s.append (text, Font (tooltipFontSize, Font::bold)); + TextLayout tl; + tl.createLayoutWithBalancedLineLengths (s, (float) maxToolTipWidth); return tl; } } @@ -527,7 +529,6 @@ void LookAndFeel::drawAlertBox (Graphics& g, g.fillAll (alert.findColour (AlertWindow::backgroundColourId)); int iconSpaceUsed = 0; - Justification alignment (Justification::horizontallyCentred); const int iconWidth = 80; int iconSize = jmin (iconWidth + 50, alert.getHeight() + 20); @@ -577,15 +578,14 @@ void LookAndFeel::drawAlertBox (Graphics& g, g.fillPath (icon); iconSpaceUsed = iconWidth; - alignment = Justification::left; } g.setColour (alert.findColour (AlertWindow::textColourId)); - textLayout.drawWithin (g, - textArea.getX() + iconSpaceUsed, textArea.getY(), - textArea.getWidth() - iconSpaceUsed, textArea.getHeight(), - alignment.getFlags() | Justification::top); + textLayout.draw (g, Rectangle (textArea.getX() + iconSpaceUsed, + textArea.getY(), + textArea.getWidth() - iconSpaceUsed, + textArea.getHeight()).toFloat()); g.setColour (alert.findColour (AlertWindow::outlineColourId)); g.drawRect (0, 0, alert.getWidth(), alert.getHeight()); @@ -1657,8 +1657,8 @@ void LookAndFeel::getTooltipSize (const String& tipText, int& width, int& height { const TextLayout tl (LookAndFeelHelpers::layoutTooltipText (tipText)); - width = tl.getWidth() + 14; - height = tl.getHeight() + 6; + width = (int) (tl.getWidth() + 14.0f); + height = (int) (tl.getHeight() + 6.0f); } void LookAndFeel::drawTooltip (Graphics& g, const String& text, int width, int height) @@ -1673,7 +1673,7 @@ void LookAndFeel::drawTooltip (Graphics& g, const String& text, int width, int h const TextLayout tl (LookAndFeelHelpers::layoutTooltipText (text)); g.setColour (findColour (TooltipWindow::textColourId)); - tl.drawWithin (g, 0, 0, width, height, Justification::centred); + tl.draw (g, Rectangle (0.0f, 0.0f, (float) width, (float) height)); } //============================================================================== diff --git a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp index 19f3376c21..b28c01a56b 100644 --- a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp +++ b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp @@ -117,10 +117,10 @@ void AlertWindow::setMessage (const String& message) font = getLookAndFeel().getAlertWindowMessageFont(); - Font titleFont (font.getHeight() * 1.1f, Font::bold); - textLayout.setText (getName() + "\n\n", titleFont); - - textLayout.appendText (text, font); + AttributedString newText; + newText.append (getName() + "\n\n", Font (font.getHeight() * 1.1f, Font::bold)); + newText.append (text, font); + attributedText = newText; updateLayout (true); repaint(); @@ -271,10 +271,13 @@ public: void updateLayout (const int width) { + AttributedString s; + s.setJustification (Justification::topLeft); + s.append (getName(), getFont()); + TextLayout text; - text.appendText (getText(), getFont()); - text.layout (width - 8, Justification::topLeft, true); - setSize (width, jmin (width, text.getHeight() + (int) getFont().getHeight())); + text.createLayoutWithBalancedLineLengths (s, width - 8.0f); + setSize (width, jmin (width, (int) (text.getHeight() + getFont().getHeight()))); } private: @@ -399,18 +402,20 @@ void AlertWindow::updateLayout (const bool onlyIncreaseSize) if (alertIconType == NoIcon) { - textLayout.layout (w, Justification::horizontallyCentred, true); + attributedText.setJustification (Justification::centredTop); + textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w); } else { - textLayout.layout (w, Justification::left, true); + attributedText.setJustification (Justification::topLeft); + textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w); iconSpace = iconWidth; } - w = jmax (350, textLayout.getWidth() + iconSpace + edgeGap * 4); + w = jmax (350, (int) textLayout.getWidth() + iconSpace + edgeGap * 4); w = jmin (w, (int) (getParentWidth() * 0.7f)); - const int textLayoutH = textLayout.getHeight(); + const int textLayoutH = (int) textLayout.getHeight(); const int textBottom = 16 + titleH + textLayoutH; int h = textBottom; diff --git a/modules/juce_gui_basics/windows/juce_AlertWindow.h b/modules/juce_gui_basics/windows/juce_AlertWindow.h index b171bfe321..7637768c5c 100644 --- a/modules/juce_gui_basics/windows/juce_AlertWindow.h +++ b/modules/juce_gui_basics/windows/juce_AlertWindow.h @@ -446,6 +446,7 @@ protected: private: //============================================================================== String text; + AttributedString attributedText; TextLayout textLayout; AlertIconType alertIconType; ComponentBoundsConstrainer constrainer; diff --git a/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.cpp b/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.cpp index fb16581681..fc457070b6 100644 --- a/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.cpp +++ b/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.cpp @@ -43,12 +43,8 @@ void BubbleMessageComponent::showAt (int x, int y, const bool removeWhenMouseClicked, const bool deleteSelfAfterUse) { - textLayout.clear(); - textLayout.setText (text, Font (14.0f)); - textLayout.layout (256, Justification::centredLeft, true); - + createLayout (text); setPosition (x, y); - init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse); } @@ -58,15 +54,20 @@ void BubbleMessageComponent::showAt (Component* const component, const bool removeWhenMouseClicked, const bool deleteSelfAfterUse) { - textLayout.clear(); - textLayout.setText (text, Font (14.0f)); - textLayout.layout (256, Justification::centredLeft, true); - + createLayout (text); setPosition (component); - init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse); } +void BubbleMessageComponent::createLayout (const String& text) +{ + AttributedString attString; + attString.append (text, Font (14.0f)); + attString.setJustification (Justification::centred); + + textLayout.createLayoutWithBalancedLineLengths (attString, 256); +} + void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving, const bool removeWhenMouseClicked, const bool deleteSelfAfterUse) @@ -92,15 +93,15 @@ void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving, void BubbleMessageComponent::getContentSize (int& w, int& h) { - w = textLayout.getWidth() + 16; - h = textLayout.getHeight() + 16; + w = (int) (textLayout.getWidth() + 16.0f); + h = (int) (textLayout.getHeight() + 16.0f); } void BubbleMessageComponent::paintContent (Graphics& g, int w, int h) { g.setColour (findColour (TooltipWindow::textColourId)); - textLayout.drawWithin (g, 0, 0, w, h, Justification::centred); + textLayout.draw (g, Rectangle (0.0f, 0.0f, (float) w, (float) h)); } void BubbleMessageComponent::timerCallback() diff --git a/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.h b/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.h index 166c489c93..67550f88e4 100644 --- a/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.h +++ b/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.h @@ -121,6 +121,7 @@ private: int64 expiryTime; bool deleteAfterUse; + void createLayout (const String&); void init (int numMillisecondsBeforeRemoving, bool removeWhenMouseClicked, bool deleteSelfAfterUse);