| @@ -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; | |||
| }; | |||
| @@ -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<int> r (getTipArea()); | |||
| tl.drawWithin (g, r.getX(), r.getY(), r.getWidth(), r.getHeight(), | |||
| Justification::bottomLeft); | |||
| tl.draw (g, getLocalBounds().toFloat()); | |||
| } | |||
| void PropertyPanelWithTooltips::resized() | |||
| @@ -62,7 +62,6 @@ public: | |||
| private: | |||
| PropertyPanel panel; | |||
| TextLayout layout; | |||
| Component* lastComp; | |||
| String lastTip; | |||
| @@ -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<int> (12, getHeight() - 130, getWidth() - 24, 100).toFloat()); | |||
| text2.draw (g, Rectangle<int> (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; | |||
| }; | |||
| @@ -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<int>& range, const Colour& colour) | |||
| { | |||
| attributes.add (new Attribute (range, colour)); | |||
| } | |||
| void AttributedString::setFont (const Range<int>& range, const Font& font) | |||
| { | |||
| attributes.add (new Attribute (range, font)); | |||
| } | |||
| void AttributedString::draw (Graphics& g, const Rectangle<float>& 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<float>& anchor_) noexcept | |||
| : glyphCode (glyphCode_), anchor (anchor_) | |||
| { | |||
| } | |||
| GlyphLayout::Glyph::~Glyph() {} | |||
| //============================================================================== | |||
| GlyphLayout::Run::Run() | |||
| : colour (0xff000000) | |||
| { | |||
| } | |||
| GlyphLayout::Run::Run (const Range<int>& range, const int numGlyphsToPreallocate) | |||
| : stringRange (range), colour (0xff000000) | |||
| #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS | |||
| AttributedString::AttributedString (AttributedString&& other) noexcept | |||
| : text (static_cast <String&&> (other.text)), | |||
| lineSpacing (other.lineSpacing), | |||
| justification (other.justification), | |||
| wordWrap (other.wordWrap), | |||
| readingDirection (other.readingDirection), | |||
| attributes (static_cast <OwnedArray<Attribute>&&> (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 <String&&> (other.text); | |||
| lineSpacing = other.lineSpacing; | |||
| justification = other.justification; | |||
| wordWrap = other.wordWrap; | |||
| readingDirection = other.readingDirection; | |||
| attributes = static_cast <OwnedArray<Attribute>&&> (other.attributes); | |||
| return *this; | |||
| } | |||
| #endif | |||
| void GlyphLayout::Run::ensureStorageAllocated (int numGlyphsNeeded) | |||
| { | |||
| glyphs.ensureStorageAllocated (numGlyphsNeeded); | |||
| } | |||
| AttributedString::~AttributedString() {} | |||
| void GlyphLayout::Run::setStringRange (const Range<int>& 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<int> (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<int>& stringRange_, const Point<float>& 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<int> (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<int> (oldLength, oldLength + newLength), font); | |||
| setColour (Range<int> (oldLength, oldLength + newLength), colour); | |||
| } | |||
| void GlyphLayout::Line::setStringRange (const Range<int>& newStringRange) noexcept | |||
| void AttributedString::clear() | |||
| { | |||
| stringRange = newStringRange; | |||
| text = String::empty; | |||
| attributes.clear(); | |||
| } | |||
| void GlyphLayout::Line::setLineOrigin (const Point<float>& 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<int>& 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<int> (0, text.length()), colour); | |||
| } | |||
| GlyphLayout::Line& GlyphLayout::getLine (const int index) const | |||
| void AttributedString::setFont (const Range<int>& 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<int> (0, text.length()), font); | |||
| } | |||
| void GlyphLayout::draw (Graphics& g, const Rectangle<float>& area) const | |||
| void AttributedString::draw (Graphics& g, const Rectangle<float>& area) const | |||
| { | |||
| const Point<float> origin (justification.appliedToRectangle (Rectangle<float> (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<float> 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<int>& range_) noexcept | |||
| : fontAndColour (fontAndColour_), range (range_) | |||
| {} | |||
| FontAndColour fontAndColour; | |||
| Range<int> 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<int> 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<float> tokenPos (t->area.getPosition().toFloat()); | |||
| Array <int> newGlyphs; | |||
| Array <float> 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<float> (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<int> (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<int> (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<int> (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<int>& 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<RunAttribute> 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<int> (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<Token> tokens; | |||
| int totalLines; | |||
| JUCE_DECLARE_NON_COPYABLE (TokenList); | |||
| }; | |||
| } | |||
| //============================================================================== | |||
| void GlyphLayout::createStandardLayout (const AttributedString& text) | |||
| { | |||
| GlyphLayoutHelpers::TokenList l; | |||
| l.createLayout (text, *this); | |||
| } | |||
| END_JUCE_NAMESPACE | |||
| @@ -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<int> range; | |||
| private: | |||
| @@ -165,9 +186,15 @@ public: | |||
| /** Adds a colour attribute for the specified range. */ | |||
| void setColour (const Range<int>& 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<int>& 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<Attribute> 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<float>& area) const; | |||
| //============================================================================== | |||
| /** A positioned glyph. */ | |||
| class JUCE_API Glyph | |||
| { | |||
| public: | |||
| Glyph (int glyphCode, const Point<float>& anchor) noexcept; | |||
| ~Glyph(); | |||
| const int glyphCode; | |||
| const Point<float> anchor; | |||
| private: | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Glyph); | |||
| }; | |||
| //============================================================================== | |||
| /** A sequence of glyphs. */ | |||
| class JUCE_API Run | |||
| { | |||
| public: | |||
| Run(); | |||
| Run (const Range<int>& 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<int>& newStringRange) noexcept; | |||
| void setFont (const Font& newFont); | |||
| void setColour (const Colour& newColour) noexcept; | |||
| void addGlyph (Glyph* glyph); | |||
| void ensureStorageAllocated (int numGlyphsNeeded); | |||
| private: | |||
| OwnedArray<Glyph> glyphs; | |||
| Range<int> 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<int>& stringRange, const Point<float>& lineOrigin, | |||
| float ascent, float descent, float leading, int numRunsToPreallocate); | |||
| ~Line(); | |||
| const Point<float>& 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<int>& newStringRange) noexcept; | |||
| void setLineOrigin (const Point<float>& newLineOrigin) noexcept; | |||
| void setLeading (float newLeading) noexcept; | |||
| void increaseAscentDescent (float newAscent, float newDescent) noexcept; | |||
| void addRun (Run* run); | |||
| private: | |||
| OwnedArray<Run> runs; | |||
| Range<int> stringRange; | |||
| Point<float> 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<Line> 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__ | |||
| @@ -25,351 +25,589 @@ | |||
| BEGIN_JUCE_NAMESPACE | |||
| TextLayout::Glyph::Glyph (const int glyphCode_, const Point<float>& 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<int>& 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<int>& stringRange_, const Point<float>& 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<float> TextLayout::Line::getLineBoundsX() const noexcept | |||
| { | |||
| Range<float> 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<float> (minX, maxX); | |||
| } | |||
| else | |||
| { | |||
| range = range.getUnionWith (Range<float> (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 <OwnedArray<Line>&&> (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 <OwnedArray<Line>&&> (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<float>& area) const | |||
| { | |||
| const Point<float> origin (justification.appliedToRectangle (Rectangle<float> (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<float> 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<int>& range_) noexcept | |||
| : fontAndColour (fontAndColour_), range (range_) | |||
| {} | |||
| while (maxWidth > originalW / 2) | |||
| FontAndColour fontAndColour; | |||
| Range<int> 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<int> 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<float> tokenPos (t->area.getPosition().toFloat()); | |||
| maxWidth -= 10; | |||
| } | |||
| Array <int> newGlyphs; | |||
| Array <float> 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<float> (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<int> (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<int> (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<int> (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<int>& 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<RunAttribute> 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<int> (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<Token> 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<float> 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 | |||
| @@ -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<float>& 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<float>& 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<float> 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<int>& stringRange, int numGlyphsToPreallocate); | |||
| ~Run() noexcept; | |||
| Font font; /**< The run's font. */ | |||
| Colour colour; /**< The run's colour. */ | |||
| Array<Glyph> glyphs; /**< The glyphs in this run. */ | |||
| Range<int> 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<int>& stringRange, const Point<float>& 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<float> getLineBoundsX() const noexcept; | |||
| OwnedArray<Run> runs; /**< The glyph-runs in this line. */ | |||
| Range<int> stringRange; /**< The character range that this line represents in the | |||
| original string that was used to create it. */ | |||
| Point<float> 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 <Token>; | |||
| OwnedArray <Token> tokens; | |||
| int totalLines; | |||
| OwnedArray<Line> lines; | |||
| float width; | |||
| Justification justification; | |||
| JUCE_LEAK_DETECTOR (TextLayout); | |||
| void createStandardLayout (const AttributedString&); | |||
| bool createNativeLayout (const AttributedString&); | |||
| void recalculateWidth(); | |||
| }; | |||
| #endif // __JUCE_TEXTLAYOUT_JUCEHEADER__ | |||
| @@ -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 | |||
| @@ -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<CGSize> 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<CGGlyph> 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<CGPoint> 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> ((int) runStringRange.location, | |||
| (int) (runStringRange.location + runStringRange.length - 1)), | |||
| (int) numGlyphs); | |||
| glyphLine->addRun (glyphRun); | |||
| TextLayout::Run* const glyphRun = new TextLayout::Run (Range<int> ((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 <CGGlyph> glyphBuffer; | |||
| HeapBlock <CGPoint> 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<float> (posPtr[k].x, posPtr[k].y))); | |||
| glyphRun->glyphs.add (TextLayout::Glyph (glyphs.glyphs[k], Point<float> (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 <CGSize> 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 <CGSize> advances (length); | |||
| CTRunGetAdvances (run, CFRangeMake (0, 0), advances); | |||
| HeapBlock <CGGlyph> 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); | |||
| @@ -65,7 +65,7 @@ namespace DirectWriteTypeLayout | |||
| DWRITE_GLYPH_RUN const* glyphRun, DWRITE_GLYPH_RUN_DESCRIPTION const* runDescription, | |||
| IUnknown* clientDrawingEffect) | |||
| { | |||
| GlyphLayout* const glyphLayout = static_cast<GlyphLayout*> (clientDrawingContext); | |||
| TextLayout* const layout = static_cast<TextLayout*> (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<float> (baselineOriginX, baselineOriginY)); | |||
| layout->getLine (currentLine).lineOrigin = Point<float> (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<int> (runDescription->textPosition, | |||
| runDescription->textPosition + runDescription->stringLength), | |||
| glyphRun->glyphCount); | |||
| glyphLine.addRun (glyphRunLayout); | |||
| TextLayout::Run* const glyphRunLayout = new TextLayout::Run (Range<int> (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<ID2D1SolidColorBrush*> (clientDrawingEffect))); | |||
| glyphRunLayout->font = Font (fontName, glyphRun->fontEmSize / fontHeightToEmSizeFactor, styleFlags); | |||
| glyphRunLayout->colour = getColourOf (static_cast<ID2D1SolidColorBrush*> (clientDrawingEffect)); | |||
| const Point<float> lineOrigin (glyphLayout->getLine (currentLine).getLineOrigin()); | |||
| const Point<float> 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<float> (x, baselineOriginY - lineOrigin.y))); | |||
| glyphRunLayout->glyphs.add (TextLayout::Glyph (glyphRun->glyphIndices[i], | |||
| Point<float> (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<IDWriteTextLayout> 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 <DWRITE_LINE_METRICS> dwLineMetrics (actualLineCount); | |||
| hr = dwTextLayout->GetLineMetrics (dwLineMetrics, actualLineCount, &actualLineCount); | |||
| @@ -318,19 +321,19 @@ namespace DirectWriteTypeLayout | |||
| const Range<int> 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<CustomDirectWriteTextRenderer> 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(); | |||
| @@ -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<int> (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<float> (0.0f, 0.0f, (float) width, (float) height)); | |||
| } | |||
| //============================================================================== | |||
| @@ -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; | |||
| @@ -446,6 +446,7 @@ protected: | |||
| private: | |||
| //============================================================================== | |||
| String text; | |||
| AttributedString attributedText; | |||
| TextLayout textLayout; | |||
| AlertIconType alertIconType; | |||
| ComponentBoundsConstrainer constrainer; | |||
| @@ -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<float> (0.0f, 0.0f, (float) w, (float) h)); | |||
| } | |||
| void BubbleMessageComponent::timerCallback() | |||
| @@ -121,6 +121,7 @@ private: | |||
| int64 expiryTime; | |||
| bool deleteAfterUse; | |||
| void createLayout (const String&); | |||
| void init (int numMillisecondsBeforeRemoving, | |||
| bool removeWhenMouseClicked, | |||
| bool deleteSelfAfterUse); | |||