| @@ -730,16 +730,16 @@ public: | |||||
| void paint (Graphics& g) | 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; | 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) | 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() | void timerCallback() | ||||
| @@ -785,7 +785,6 @@ private: | |||||
| return String::empty; | return String::empty; | ||||
| } | } | ||||
| TextLayout layout; | |||||
| Component* lastComp; | Component* lastComp; | ||||
| String lastTip; | String lastTip; | ||||
| }; | }; | ||||
| @@ -231,18 +231,16 @@ PropertyPanelWithTooltips::~PropertyPanelWithTooltips() | |||||
| void PropertyPanelWithTooltips::paint (Graphics& g) | 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; | 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) | 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() | void PropertyPanelWithTooltips::resized() | ||||
| @@ -62,7 +62,6 @@ public: | |||||
| private: | private: | ||||
| PropertyPanel panel; | PropertyPanel panel; | ||||
| TextLayout layout; | |||||
| Component* lastComp; | Component* lastComp; | ||||
| String lastTip; | String lastTip; | ||||
| @@ -29,88 +29,80 @@ | |||||
| //============================================================================== | //============================================================================== | ||||
| class MiscPage : public Component | class MiscPage : public Component | ||||
| { | { | ||||
| FilenameComponent* templateDir; | |||||
| public: | public: | ||||
| MiscPage() | 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() | ~MiscPage() | ||||
| { | { | ||||
| StoredSettings::getInstance()->setTemplatesDir (templateDir->getCurrentFile()); | |||||
| deleteAllChildren(); | |||||
| StoredSettings::getInstance()->setTemplatesDir (templateDir.getCurrentFile()); | |||||
| } | } | ||||
| void resized() | 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 | class AboutPage : public Component | ||||
| { | { | ||||
| HyperlinkButton* link; | |||||
| Image logo; | |||||
| TextLayout text1, text2; | |||||
| public: | public: | ||||
| AboutPage() | 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) | void paint (Graphics& g) | ||||
| { | { | ||||
| g.fillAll (Colour (0xffebebeb)); | g.fillAll (Colour (0xffebebeb)); | ||||
| g.drawImageWithin (logo, 0, 4, getWidth(), getHeight() - 134, | |||||
| g.drawImageWithin (logo, 0, 4, getWidth(), getHeight() - 144, | |||||
| RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize, | RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize, | ||||
| false); | 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() | 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; | 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 | END_JUCE_NAMESPACE | ||||
| @@ -34,6 +34,8 @@ | |||||
| An attributed string lets you create a string with varied fonts, colours, word-wrapping, | An attributed string lets you create a string with varied fonts, colours, word-wrapping, | ||||
| layout, etc., and draw it using AttributedString::draw(). | layout, etc., and draw it using AttributedString::draw(). | ||||
| @see TextLayout | |||||
| */ | */ | ||||
| class JUCE_API AttributedString | class JUCE_API AttributedString | ||||
| { | { | ||||
| @@ -44,8 +46,12 @@ public: | |||||
| /** Creates an attributed string with the given text. */ | /** Creates an attributed string with the given text. */ | ||||
| explicit AttributedString (const String& 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. */ | /** Destructor. */ | ||||
| ~AttributedString(); | ~AttributedString(); | ||||
| @@ -54,12 +60,27 @@ public: | |||||
| /** Returns the complete text of this attributed string. */ | /** Returns the complete text of this attributed string. */ | ||||
| const String& getText() const noexcept { return text; } | 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); | 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. | /** Draws this string within the given area. | ||||
| The layout of the string within the rectangle is controlled by the justification | 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. */ | /** If this attribute specifies a colour, this returns it; otherwise it returns nullptr. */ | ||||
| const Colour* getColour() const noexcept { return colour; } | 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; | const Range<int> range; | ||||
| private: | private: | ||||
| @@ -165,9 +186,15 @@ public: | |||||
| /** Adds a colour attribute for the specified range. */ | /** Adds a colour attribute for the specified range. */ | ||||
| void setColour (const Range<int>& range, const Colour& colour); | 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. */ | /** Adds a font attribute for the specified range. */ | ||||
| void setFont (const Range<int>& range, const Font& font); | 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: | private: | ||||
| String text; | String text; | ||||
| float lineSpacing; | float lineSpacing; | ||||
| @@ -177,142 +204,4 @@ private: | |||||
| OwnedArray<Attribute> attributes; | 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__ | #endif // __JUCE_ATTRIBUTEDSTRING_JUCEHEADER__ | ||||
| @@ -25,351 +25,589 @@ | |||||
| BEGIN_JUCE_NAMESPACE | 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() | 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; | return *this; | ||||
| } | } | ||||
| TextLayout::~TextLayout() | 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 | 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 | END_JUCE_NAMESPACE | ||||
| @@ -30,124 +30,145 @@ | |||||
| #include "../placement/juce_Justification.h" | #include "../placement/juce_Justification.h" | ||||
| class Graphics; | 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 | class JUCE_API TextLayout | ||||
| { | { | ||||
| public: | 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(); | 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. */ | /** Destructor. */ | ||||
| ~TextLayout(); | ~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: | 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__ | #endif // __JUCE_TEXTLAYOUT_JUCEHEADER__ | ||||
| @@ -46,7 +46,7 @@ | |||||
| management and layout. | management and layout. | ||||
| */ | */ | ||||
| #ifndef JUCE_USE_DIRECTWRITE | #ifndef JUCE_USE_DIRECTWRITE | ||||
| #define JUCE_USE_DIRECTWRITE 0 | |||||
| #define JUCE_USE_DIRECTWRITE 1 | |||||
| #endif | #endif | ||||
| #ifndef JUCE_INCLUDE_PNGLIB_CODE | #ifndef JUCE_INCLUDE_PNGLIB_CODE | ||||
| @@ -33,6 +33,116 @@ | |||||
| namespace CoreTextTypeLayout | 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) | CFAttributedStringRef createCFAttributedString (const AttributedString& text) | ||||
| { | { | ||||
| #if JUCE_IOS | #if JUCE_IOS | ||||
| @@ -58,17 +168,10 @@ namespace CoreTextTypeLayout | |||||
| if (attr->getFont() != nullptr) | 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()), | CFAttributedStringSetAttribute (attribString, CFRangeMake (range.getStart(), range.getLength()), | ||||
| kCTFontAttributeName, ctFontRef); | kCTFontAttributeName, ctFontRef); | ||||
| @@ -190,7 +293,7 @@ namespace CoreTextTypeLayout | |||||
| CFRelease (frame); | CFRelease (frame); | ||||
| } | } | ||||
| void createLayout (GlyphLayout& glyphLayout, const AttributedString& text) | |||||
| void createLayout (TextLayout& glyphLayout, const AttributedString& text) | |||||
| { | { | ||||
| CFAttributedStringRef attribString = CoreTextTypeLayout::createCFAttributedString (text); | CFAttributedStringRef attribString = CoreTextTypeLayout::createCFAttributedString (text); | ||||
| CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString (attribString); | CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString (attribString); | ||||
| @@ -228,9 +331,9 @@ namespace CoreTextTypeLayout | |||||
| CGFloat ascent, descent, leading; | CGFloat ascent, descent, leading; | ||||
| CTLineGetTypographicBounds (line, &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); | glyphLayout.addLine (glyphLine); | ||||
| for (CFIndex j = 0; j < numRuns; ++j) | for (CFIndex j = 0; j < numRuns; ++j) | ||||
| @@ -239,10 +342,10 @@ namespace CoreTextTypeLayout | |||||
| const CFIndex numGlyphs = CTRunGetGlyphCount (run); | const CFIndex numGlyphs = CTRunGetGlyphCount (run); | ||||
| const CFRange runStringRange = CTRunGetStringRange (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); | CFDictionaryRef runAttributes = CTRunGetAttributes (run); | ||||
| @@ -250,18 +353,16 @@ namespace CoreTextTypeLayout | |||||
| if (CFDictionaryGetValueIfPresent (runAttributes, kCTFontAttributeName, (const void **) &ctRunFont)) | if (CFDictionaryGetValueIfPresent (runAttributes, kCTFontAttributeName, (const void **) &ctRunFont)) | ||||
| { | { | ||||
| CFStringRef cfsFontName = CTFontCopyPostScriptName (ctRunFont); | CFStringRef cfsFontName = CTFontCopyPostScriptName (ctRunFont); | ||||
| const String fontName (String::fromCFString (cfsFontName)); | |||||
| CTFontRef ctFontRef = CTFontCreateWithName (cfsFontName, 1024, nullptr); | CTFontRef ctFontRef = CTFontCreateWithName (cfsFontName, 1024, nullptr); | ||||
| CFRelease (cfsFontName); | |||||
| CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr); | CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr); | ||||
| CFRelease (ctFontRef); | CFRelease (ctFontRef); | ||||
| const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef)); | const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef)); | ||||
| const float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight; | const float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight; | ||||
| CGFontRelease (cgFontRef); | 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; | CGColorRef cgRunColor; | ||||
| @@ -270,28 +371,17 @@ namespace CoreTextTypeLayout | |||||
| { | { | ||||
| const CGFloat* const components = CGColorGetComponents (cgRunColor); | 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) | 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), | ascent (0.0f), | ||||
| unitsToHeightScaleFactor (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) | 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)); | ascent = std::abs ((float) CTFontGetAscent (ctFontRef)); | ||||
| const float totalSize = ascent + std::abs ((float) CTFontGetDescent (ctFontRef)); | const float totalSize = ascent + std::abs ((float) CTFontGetDescent (ctFontRef)); | ||||
| ascent /= totalSize; | ascent /= totalSize; | ||||
| @@ -411,11 +471,11 @@ public: | |||||
| { | { | ||||
| CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i); | CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i); | ||||
| CFIndex length = CTRunGetGlyphCount (run); | 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) | for (int j = 0; j < length; ++j) | ||||
| x += (float) advances[j].width; | |||||
| x += (float) advances.advances[j].width; | |||||
| } | } | ||||
| CFRelease (line); | CFRelease (line); | ||||
| @@ -446,16 +506,15 @@ public: | |||||
| { | { | ||||
| CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i); | CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i); | ||||
| CFIndex length = CTRunGetGlyphCount (run); | 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) | for (int j = 0; j < length; ++j) | ||||
| { | { | ||||
| x += (float) advances[j].width; | |||||
| x += (float) advances.advances[j].width; | |||||
| xOffsets.add (x * unitsToHeightScaleFactor); | 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); | return Typeface::createSystemTypefaceFor (f); | ||||
| } | } | ||||
| bool GlyphLayout::createNativeLayout (const AttributedString& text) | |||||
| bool TextLayout::createNativeLayout (const AttributedString& text) | |||||
| { | { | ||||
| #if JUCE_CORETEXT_AVAILABLE | #if JUCE_CORETEXT_AVAILABLE | ||||
| CoreTextTypeLayout::createLayout (*this, text); | CoreTextTypeLayout::createLayout (*this, text); | ||||
| @@ -65,7 +65,7 @@ namespace DirectWriteTypeLayout | |||||
| DWRITE_GLYPH_RUN const* glyphRun, DWRITE_GLYPH_RUN_DESCRIPTION const* runDescription, | DWRITE_GLYPH_RUN const* glyphRun, DWRITE_GLYPH_RUN_DESCRIPTION const* runDescription, | ||||
| IUnknown* clientDrawingEffect) | IUnknown* clientDrawingEffect) | ||||
| { | { | ||||
| GlyphLayout* const glyphLayout = static_cast<GlyphLayout*> (clientDrawingContext); | |||||
| TextLayout* const layout = static_cast<TextLayout*> (clientDrawingContext); | |||||
| if (baselineOriginY != lastOriginY) | if (baselineOriginY != lastOriginY) | ||||
| { | { | ||||
| @@ -73,49 +73,52 @@ namespace DirectWriteTypeLayout | |||||
| ++currentLine; | ++currentLine; | ||||
| // The x value is only correct when dealing with LTR text | // 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) | if (currentLine < 0) | ||||
| return S_OK; | return S_OK; | ||||
| GlyphLayout::Line& glyphLine = glyphLayout->getLine (currentLine); | |||||
| TextLayout::Line& glyphLine = layout->getLine (currentLine); | |||||
| DWRITE_FONT_METRICS dwFontMetrics; | DWRITE_FONT_METRICS dwFontMetrics; | ||||
| glyphRun->fontFace->GetMetrics (&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; | int styleFlags = 0; | ||||
| const String fontName (getFontName (glyphRun, styleFlags)); | 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); | glyphRun->fontFace->GetMetrics (&dwFontMetrics); | ||||
| const float totalHeight = std::abs ((float) dwFontMetrics.ascent) + std::abs ((float) dwFontMetrics.descent); | const float totalHeight = std::abs ((float) dwFontMetrics.ascent) + std::abs ((float) dwFontMetrics.descent); | ||||
| const float fontHeightToEmSizeFactor = (float) dwFontMetrics.designUnitsPerEm / totalHeight; | 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; | float x = baselineOriginX - lineOrigin.x; | ||||
| for (UINT32 i = 0; i < glyphRun->glyphCount; ++i) | for (UINT32 i = 0; i < glyphRun->glyphCount; ++i) | ||||
| { | { | ||||
| const float advance = glyphRun->glyphAdvances[i]; | |||||
| if ((glyphRun->bidiLevel & 1) != 0) | 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) | if ((glyphRun->bidiLevel & 1) == 0) | ||||
| x += glyphRun->glyphAdvances[i]; // LTR text | |||||
| x += advance; // LTR text | |||||
| } | } | ||||
| return S_OK; | 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) | ID2D1Factory* const direct2dFactory, IDWriteFontCollection* const fontCollection) | ||||
| { | { | ||||
| // To add color to text, we need to create a D2D render target | // To add color to text, we need to create a D2D render target | ||||
| @@ -296,7 +299,7 @@ namespace DirectWriteTypeLayout | |||||
| ComSmartPtr<IDWriteTextLayout> dwTextLayout; | ComSmartPtr<IDWriteTextLayout> dwTextLayout; | ||||
| hr = directWriteFactory->CreateTextLayout (text.getText().toWideCharPointer(), textLen, | hr = directWriteFactory->CreateTextLayout (text.getText().toWideCharPointer(), textLen, | ||||
| dwTextFormat, glyphLayout.getWidth(), | |||||
| dwTextFormat, layout.getWidth(), | |||||
| 1.0e7f, dwTextLayout.resetAndGetPointerAddress()); | 1.0e7f, dwTextLayout.resetAndGetPointerAddress()); | ||||
| const int numAttributes = text.getNumAttributes(); | const int numAttributes = text.getNumAttributes(); | ||||
| @@ -307,7 +310,7 @@ namespace DirectWriteTypeLayout | |||||
| UINT32 actualLineCount = 0; | UINT32 actualLineCount = 0; | ||||
| hr = dwTextLayout->GetLineMetrics (nullptr, 0, &actualLineCount); | hr = dwTextLayout->GetLineMetrics (nullptr, 0, &actualLineCount); | ||||
| glyphLayout.ensureStorageAllocated (actualLineCount); | |||||
| layout.ensureStorageAllocated (actualLineCount); | |||||
| HeapBlock <DWRITE_LINE_METRICS> dwLineMetrics (actualLineCount); | HeapBlock <DWRITE_LINE_METRICS> dwLineMetrics (actualLineCount); | ||||
| hr = dwTextLayout->GetLineMetrics (dwLineMetrics, actualLineCount, &actualLineCount); | hr = dwTextLayout->GetLineMetrics (dwLineMetrics, actualLineCount, &actualLineCount); | ||||
| @@ -318,19 +321,19 @@ namespace DirectWriteTypeLayout | |||||
| const Range<int> lineStringRange (lastLocation, (int) lastLocation + dwLineMetrics[i].length); | const Range<int> lineStringRange (lastLocation, (int) lastLocation + dwLineMetrics[i].length); | ||||
| 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)); | ComSmartPtr<CustomDirectWriteTextRenderer> textRenderer (new CustomDirectWriteTextRenderer (fontCollection)); | ||||
| hr = dwTextLayout->Draw (&glyphLayout, textRenderer, 0, 0); | |||||
| hr = dwTextLayout->Draw (&layout, textRenderer, 0, 0); | |||||
| } | } | ||||
| } | } | ||||
| #endif | #endif | ||||
| bool GlyphLayout::createNativeLayout (const AttributedString& text) | |||||
| bool TextLayout::createNativeLayout (const AttributedString& text) | |||||
| { | { | ||||
| #if JUCE_USE_DIRECTWRITE | #if JUCE_USE_DIRECTWRITE | ||||
| const Direct2DFactories& factories = Direct2DFactories::getInstance(); | const Direct2DFactories& factories = Direct2DFactories::getInstance(); | ||||
| @@ -80,10 +80,10 @@ namespace LookAndFeelHelpers | |||||
| p.closeSubPath(); | 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 float sat = hasKeyboardFocus ? 1.3f : 0.9f; | ||||
| const Colour baseColour (buttonColour.withMultipliedSaturation (sat)); | const Colour baseColour (buttonColour.withMultipliedSaturation (sat)); | ||||
| @@ -96,15 +96,17 @@ namespace LookAndFeelHelpers | |||||
| return baseColour; | 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 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; | return tl; | ||||
| } | } | ||||
| } | } | ||||
| @@ -527,7 +529,6 @@ void LookAndFeel::drawAlertBox (Graphics& g, | |||||
| g.fillAll (alert.findColour (AlertWindow::backgroundColourId)); | g.fillAll (alert.findColour (AlertWindow::backgroundColourId)); | ||||
| int iconSpaceUsed = 0; | int iconSpaceUsed = 0; | ||||
| Justification alignment (Justification::horizontallyCentred); | |||||
| const int iconWidth = 80; | const int iconWidth = 80; | ||||
| int iconSize = jmin (iconWidth + 50, alert.getHeight() + 20); | int iconSize = jmin (iconWidth + 50, alert.getHeight() + 20); | ||||
| @@ -577,15 +578,14 @@ void LookAndFeel::drawAlertBox (Graphics& g, | |||||
| g.fillPath (icon); | g.fillPath (icon); | ||||
| iconSpaceUsed = iconWidth; | iconSpaceUsed = iconWidth; | ||||
| alignment = Justification::left; | |||||
| } | } | ||||
| g.setColour (alert.findColour (AlertWindow::textColourId)); | 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.setColour (alert.findColour (AlertWindow::outlineColourId)); | ||||
| g.drawRect (0, 0, alert.getWidth(), alert.getHeight()); | 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)); | 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) | 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)); | const TextLayout tl (LookAndFeelHelpers::layoutTooltipText (text)); | ||||
| g.setColour (findColour (TooltipWindow::textColourId)); | 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 = 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); | updateLayout (true); | ||||
| repaint(); | repaint(); | ||||
| @@ -271,10 +271,13 @@ public: | |||||
| void updateLayout (const int width) | void updateLayout (const int width) | ||||
| { | { | ||||
| AttributedString s; | |||||
| s.setJustification (Justification::topLeft); | |||||
| s.append (getName(), getFont()); | |||||
| TextLayout text; | 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: | private: | ||||
| @@ -399,18 +402,20 @@ void AlertWindow::updateLayout (const bool onlyIncreaseSize) | |||||
| if (alertIconType == NoIcon) | if (alertIconType == NoIcon) | ||||
| { | { | ||||
| textLayout.layout (w, Justification::horizontallyCentred, true); | |||||
| attributedText.setJustification (Justification::centredTop); | |||||
| textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| textLayout.layout (w, Justification::left, true); | |||||
| attributedText.setJustification (Justification::topLeft); | |||||
| textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w); | |||||
| iconSpace = iconWidth; | 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)); | w = jmin (w, (int) (getParentWidth() * 0.7f)); | ||||
| const int textLayoutH = textLayout.getHeight(); | |||||
| const int textLayoutH = (int) textLayout.getHeight(); | |||||
| const int textBottom = 16 + titleH + textLayoutH; | const int textBottom = 16 + titleH + textLayoutH; | ||||
| int h = textBottom; | int h = textBottom; | ||||
| @@ -446,6 +446,7 @@ protected: | |||||
| private: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| String text; | String text; | ||||
| AttributedString attributedText; | |||||
| TextLayout textLayout; | TextLayout textLayout; | ||||
| AlertIconType alertIconType; | AlertIconType alertIconType; | ||||
| ComponentBoundsConstrainer constrainer; | ComponentBoundsConstrainer constrainer; | ||||
| @@ -43,12 +43,8 @@ void BubbleMessageComponent::showAt (int x, int y, | |||||
| const bool removeWhenMouseClicked, | const bool removeWhenMouseClicked, | ||||
| const bool deleteSelfAfterUse) | const bool deleteSelfAfterUse) | ||||
| { | { | ||||
| textLayout.clear(); | |||||
| textLayout.setText (text, Font (14.0f)); | |||||
| textLayout.layout (256, Justification::centredLeft, true); | |||||
| createLayout (text); | |||||
| setPosition (x, y); | setPosition (x, y); | ||||
| init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse); | init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse); | ||||
| } | } | ||||
| @@ -58,15 +54,20 @@ void BubbleMessageComponent::showAt (Component* const component, | |||||
| const bool removeWhenMouseClicked, | const bool removeWhenMouseClicked, | ||||
| const bool deleteSelfAfterUse) | const bool deleteSelfAfterUse) | ||||
| { | { | ||||
| textLayout.clear(); | |||||
| textLayout.setText (text, Font (14.0f)); | |||||
| textLayout.layout (256, Justification::centredLeft, true); | |||||
| createLayout (text); | |||||
| setPosition (component); | setPosition (component); | ||||
| init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse); | 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, | void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving, | ||||
| const bool removeWhenMouseClicked, | const bool removeWhenMouseClicked, | ||||
| const bool deleteSelfAfterUse) | const bool deleteSelfAfterUse) | ||||
| @@ -92,15 +93,15 @@ void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving, | |||||
| void BubbleMessageComponent::getContentSize (int& w, int& h) | 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) | void BubbleMessageComponent::paintContent (Graphics& g, int w, int h) | ||||
| { | { | ||||
| g.setColour (findColour (TooltipWindow::textColourId)); | 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() | void BubbleMessageComponent::timerCallback() | ||||
| @@ -121,6 +121,7 @@ private: | |||||
| int64 expiryTime; | int64 expiryTime; | ||||
| bool deleteAfterUse; | bool deleteAfterUse; | ||||
| void createLayout (const String&); | |||||
| void init (int numMillisecondsBeforeRemoving, | void init (int numMillisecondsBeforeRemoving, | ||||
| bool removeWhenMouseClicked, | bool removeWhenMouseClicked, | ||||
| bool deleteSelfAfterUse); | bool deleteSelfAfterUse); | ||||