1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Complete rewrite of the TextLayout class, to provide better support for native platform layout functions. It now works with the AttributedString class, to provide a pre-formatted AttributedString that can be drawn.

This commit is contained in:
jules 2011-11-25 14:25:12 +00:00
parent 1a5bdda7f1
commit 58db7eb880
16 changed files with 994 additions and 1200 deletions

View file

@ -89,6 +89,29 @@ AttributedString& AttributedString::operator= (const AttributedString& other)
return *this;
}
#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))
{
}
AttributedString& AttributedString::operator= (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);
return *this;
}
#endif
AttributedString::~AttributedString() {}
void AttributedString::setText (const String& other)
@ -96,6 +119,45 @@ void AttributedString::setText (const String& other)
text = other;
}
void AttributedString::append (const String& textToAppend)
{
text += textToAppend;
}
void AttributedString::append (const String& textToAppend, const Font& font)
{
const int oldLength = text.length();
const int newLength = textToAppend.length();
text += textToAppend;
setFont (Range<int> (oldLength, oldLength + newLength), font);
}
void AttributedString::append (const String& textToAppend, const Colour& colour)
{
const int oldLength = text.length();
const int newLength = textToAppend.length();
text += textToAppend;
setColour (Range<int> (oldLength, oldLength + newLength), colour);
}
void AttributedString::append (const String& textToAppend, const Font& font, const Colour& colour)
{
const int oldLength = text.length();
const int newLength = textToAppend.length();
text += textToAppend;
setFont (Range<int> (oldLength, oldLength + newLength), font);
setColour (Range<int> (oldLength, oldLength + newLength), colour);
}
void AttributedString::clear()
{
text = String::empty;
attributes.clear();
}
void AttributedString::setJustification (const Justification& newJustification) noexcept
{
justification = newJustification;
@ -121,514 +183,40 @@ void AttributedString::setColour (const Range<int>& range, const Colour& colour)
attributes.add (new Attribute (range, colour));
}
void AttributedString::setColour (const Colour& colour)
{
for (int i = attributes.size(); --i >= 0;)
if (attributes.getUnchecked(i)->getColour() != nullptr)
attributes.remove (i);
setColour (Range<int> (0, text.length()), colour);
}
void AttributedString::setFont (const Range<int>& range, const Font& font)
{
attributes.add (new Attribute (range, font));
}
void AttributedString::setFont (const Font& font)
{
for (int i = attributes.size(); --i >= 0;)
if (attributes.getUnchecked(i)->getFont() != nullptr)
attributes.remove (i);
setFont (Range<int> (0, text.length()), 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());
TextLayout layout;
layout.createLayout (*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)
{
glyphs.ensureStorageAllocated (numGlyphsToPreallocate);
}
GlyphLayout::Run::~Run() {}
GlyphLayout::Glyph& GlyphLayout::Run::getGlyph (const int index) const
{
return *glyphs.getUnchecked (index);
}
void GlyphLayout::Run::ensureStorageAllocated (int numGlyphsNeeded)
{
glyphs.ensureStorageAllocated (numGlyphsNeeded);
}
void GlyphLayout::Run::setStringRange (const Range<int>& newStringRange) noexcept
{
stringRange = newStringRange;
}
void GlyphLayout::Run::setFont (const Font& newFont)
{
font = newFont;
}
void GlyphLayout::Run::setColour (const Colour& newColour) noexcept
{
colour = newColour;
}
void GlyphLayout::Run::addGlyph (Glyph* glyph)
{
glyphs.add (glyph);
}
//==============================================================================
GlyphLayout::Line::Line() noexcept
: ascent (0.0f), descent (0.0f), leading (0.0f)
{
}
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);
}
GlyphLayout::Line::~Line()
{
}
GlyphLayout::Run& GlyphLayout::Line::getRun (const int index) const noexcept
{
return *runs.getUnchecked (index);
}
void GlyphLayout::Line::setStringRange (const Range<int>& newStringRange) noexcept
{
stringRange = newStringRange;
}
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
{
ascent = jmax (ascent, newAscent);
descent = jmax (descent, newDescent);
}
void GlyphLayout::Line::addRun (Run* run)
{
runs.add (run);
}
//==============================================================================
GlyphLayout::GlyphLayout()
: width (0), justification (Justification::topLeft)
{
}
GlyphLayout::~GlyphLayout()
{
}
void GlyphLayout::setText (const AttributedString& text, float maxWidth)
{
lines.clear();
width = maxWidth;
justification = text.getJustification();
if (! createNativeLayout (text))
createStandardLayout (text);
}
float GlyphLayout::getHeight() const noexcept
{
const Line* const lastLine = lines.getLast();
return lastLine != nullptr ? lastLine->getLineOrigin().getY() + lastLine->getDescent()
: 0;
}
GlyphLayout::Line& GlyphLayout::getLine (const int index) const
{
return *lines[index];
}
void GlyphLayout::ensureStorageAllocated (int numLinesNeeded)
{
lines.ensureStorageAllocated (numLinesNeeded);
}
void GlyphLayout::addLine (Line* line)
{
lines.add (line);
}
void GlyphLayout::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 Line& line = getLine (i);
const Point<float> lineOrigin (origin + line.getLineOrigin());
for (int j = 0; j < line.getNumRuns(); ++j)
{
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));
}
}
}
}
//==============================================================================
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

View file

@ -34,6 +34,8 @@
An attributed string lets you create a string with varied fonts, colours, word-wrapping,
layout, etc., and draw it using AttributedString::draw().
@see TextLayout
*/
class JUCE_API AttributedString
{
@ -44,8 +46,12 @@ public:
/** Creates an attributed string with the given text. */
explicit AttributedString (const String& text);
AttributedString (const AttributedString& other);
AttributedString& operator= (const AttributedString& other);
AttributedString (const AttributedString&);
AttributedString& operator= (const AttributedString&);
#if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
AttributedString (AttributedString&&) noexcept;
AttributedString& operator= (AttributedString&&) noexcept;
#endif
/** Destructor. */
~AttributedString();
@ -54,12 +60,27 @@ public:
/** Returns the complete text of this attributed string. */
const String& getText() const noexcept { return text; }
/** Sets the text.
This will change the text, but won't affect any of the attributes that have
been added.
/** Replaces all the text.
This will change the text, but won't affect any of the colour or font attributes
that have been added.
*/
void setText (const String& newText);
/** Appends some text (with a default font and colour). */
void append (const String& textToAppend);
/** Appends some text, with a specified font, and the default colour (black). */
void append (const String& textToAppend, const Font& font);
/** Appends some text, with a specified colour, and the default font. */
void append (const String& textToAppend, const Colour& colour);
/** Appends some text, with a specified font and colour. */
void append (const String& textToAppend, const Font& font, const Colour& colour);
/** Resets the string, clearing all text and attributes.
Note that this won't affect global settings like the justification type,
word-wrap mode, etc.
*/
void clear();
//==============================================================================
/** Draws this string within the given area.
The layout of the string within the rectangle is controlled by the justification
@ -143,7 +164,7 @@ public:
/** If this attribute specifies a colour, this returns it; otherwise it returns nullptr. */
const Colour* getColour() const noexcept { return colour; }
/** The range of characters to which this attribute should be applied. */
/** The range of characters to which this attribute will be applied. */
const Range<int> range;
private:
@ -165,9 +186,15 @@ public:
/** Adds a colour attribute for the specified range. */
void setColour (const Range<int>& range, const Colour& colour);
/** Removes all existing colour attributes, and applies this colour to the whole string. */
void setColour (const Colour& colour);
/** Adds a font attribute for the specified range. */
void setFont (const Range<int>& range, const Font& font);
/** Removes all existing font attributes, and applies this font to the whole string. */
void setFont (const Font& font);
private:
String text;
float lineSpacing;
@ -177,142 +204,4 @@ private:
OwnedArray<Attribute> attributes;
};
//==============================================================================
/**
*/
class JUCE_API GlyphLayout
{
public:
/** Creates an empty layout. */
GlyphLayout();
/** Destructor. */
~GlyphLayout();
//==============================================================================
/** Creates a layout from the given attributed string.
This will replace any data that is currently stored in the layout.
*/
void setText (const AttributedString& text, float maxWidth);
/** Draws the layout within the specified area.
The position of the text within the rectangle is controlled by the justification
flags set in the original AttributedString that was used to create this layout.
*/
void draw (Graphics& g, const Rectangle<float>& area) const;
//==============================================================================
/** A positioned glyph. */
class JUCE_API Glyph
{
public:
Glyph (int glyphCode, const Point<float>& anchor) noexcept;
~Glyph();
const int glyphCode;
const Point<float> anchor;
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Glyph);
};
//==============================================================================
/** A sequence of glyphs. */
class JUCE_API Run
{
public:
Run();
Run (const Range<int>& range, int numGlyphsToPreallocate);
~Run();
int getNumGlyphs() const noexcept { return glyphs.size(); }
const Font& getFont() const noexcept { return font; }
const Colour& getColour() const { return colour; }
Glyph& getGlyph (int index) const;
void setStringRange (const Range<int>& newStringRange) noexcept;
void setFont (const Font& newFont);
void setColour (const Colour& newColour) noexcept;
void addGlyph (Glyph* glyph);
void ensureStorageAllocated (int numGlyphsNeeded);
private:
OwnedArray<Glyph> glyphs;
Range<int> stringRange;
Font font;
Colour colour;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Run);
};
//==============================================================================
/** A line containing a sequence of glyph-runs. */
class JUCE_API Line
{
public:
Line() noexcept;
Line (const Range<int>& stringRange, const Point<float>& lineOrigin,
float ascent, float descent, float leading, int numRunsToPreallocate);
~Line();
const Point<float>& getLineOrigin() const noexcept { return lineOrigin; }
float getAscent() const noexcept { return ascent; }
float getDescent() const noexcept { return descent; }
float getLeading() const noexcept { return leading; }
int getNumRuns() const noexcept { return runs.size(); }
Run& getRun (int index) const noexcept;
void setStringRange (const Range<int>& newStringRange) noexcept;
void setLineOrigin (const Point<float>& newLineOrigin) noexcept;
void setLeading (float newLeading) noexcept;
void increaseAscentDescent (float newAscent, float newDescent) noexcept;
void addRun (Run* run);
private:
OwnedArray<Run> runs;
Range<int> stringRange;
Point<float> lineOrigin;
float ascent, descent, leading;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Line);
};
//==============================================================================
/** Returns the maximum width of the content. */
float getWidth() const noexcept { return width; }
/** Returns the maximum height of the content. */
float getHeight() const noexcept;
/** Returns the number of lines in the layout. */
int getNumLines() const noexcept { return lines.size(); }
/** Returns one of the lines. */
Line& getLine (int index) const;
/** Adds a line to the layout. The object passed-in will be owned and deleted by the layout
when it is no longer needed.
*/
void addLine (Line* line);
/** Pre-allocates space for the specified number of lines. */
void ensureStorageAllocated (int numLinesNeeded);
private:
OwnedArray<Line> lines;
float width;
Justification justification;
void createStandardLayout (const AttributedString&);
bool createNativeLayout (const AttributedString&);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GlyphLayout);
};
#endif // __JUCE_ATTRIBUTEDSTRING_JUCEHEADER__

View file

@ -25,351 +25,589 @@
BEGIN_JUCE_NAMESPACE
//==============================================================================
class TextLayout::Token
TextLayout::Glyph::Glyph (const int glyphCode_, const Point<float>& anchor_, float width_) noexcept
: glyphCode (glyphCode_), anchor (anchor_), width (width_)
{
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::Glyph::Glyph (const Glyph& other) noexcept
: glyphCode (other.glyphCode), anchor (other.anchor), width (other.width)
{
}
void draw (Graphics& g,
const int xOffset,
const int yOffset)
TextLayout::Glyph& TextLayout::Glyph::operator= (const Glyph& other) noexcept
{
glyphCode = other.glyphCode;
anchor = other.anchor;
width = other.width;
return *this;
}
TextLayout::Glyph::~Glyph() noexcept {}
//==============================================================================
TextLayout::Run::Run() noexcept
: colour (0xff000000)
{
}
TextLayout::Run::Run (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 {}
//==============================================================================
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;
for (int j = run->glyphs.size(); --j > 0;)
{
const Glyph& glyph = run->glyphs.getReference (j);
const float x = glyph.anchor.x;
minX = jmin (minX, x);
maxX = jmax (maxX, x + glyph.width);
}
if (isFirst)
{
isFirst = false;
range = Range<float> (minX, maxX);
}
else
{
range = range.getUnionWith (Range<float> (minX, maxX));
}
}
}
String text;
Font font;
int x, y, w, h;
int line, lineHeight;
bool isWhitespace, isNewLine;
private:
JUCE_LEAK_DETECTOR (Token);
};
return range + lineOrigin.x;
}
//==============================================================================
TextLayout::TextLayout()
: totalLines (0)
: width (0), justification (Justification::topLeft)
{
tokens.ensureStorageAllocated (64);
}
TextLayout::TextLayout (const String& text, const Font& font)
: totalLines (0)
{
tokens.ensureStorageAllocated (64);
appendText (text, font);
}
TextLayout::TextLayout (const TextLayout& other)
: totalLines (0)
: width (other.width),
justification (other.justification)
{
*this = other;
lines.addCopiesOf (other.lines);
}
#if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
TextLayout::TextLayout (TextLayout&& other) noexcept
: lines (static_cast <OwnedArray<Line>&&> (other.lines)),
width (other.width),
justification (other.justification)
{
}
TextLayout& TextLayout::operator= (TextLayout&& other) noexcept
{
lines = static_cast <OwnedArray<Line>&&> (other.lines);
width = other.width;
justification = other.justification;
return *this;
}
#endif
TextLayout& TextLayout::operator= (const TextLayout& other)
{
if (this != &other)
{
clear();
totalLines = other.totalLines;
tokens.addCopiesOf (other.tokens);
}
width = other.width;
justification = other.justification;
lines.clear();
lines.addCopiesOf (other.lines);
return *this;
}
TextLayout::~TextLayout()
{
clear();
}
float TextLayout::getHeight() const noexcept
{
const Line* const lastLine = lines.getLast();
return lastLine != nullptr ? lastLine->lineOrigin.y + lastLine->descent
: 0;
}
TextLayout::Line& TextLayout::getLine (const int index) const
{
return *lines[index];
}
void TextLayout::ensureStorageAllocated (int numLinesNeeded)
{
lines.ensureStorageAllocated (numLinesNeeded);
}
void TextLayout::addLine (Line* line)
{
lines.add (line);
}
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 Line& line = getLine (i);
const Point<float> lineOrigin (origin + line.lineOrigin);
for (int j = 0; j < line.runs.size(); ++j)
{
const Run* const run = line.runs.getUnchecked (j);
jassert (run != nullptr);
context.setFont (run->font);
context.setFill (run->colour);
for (int k = 0; k < run->glyphs.size(); ++k)
{
const Glyph& glyph = run->glyphs.getReference (k);
context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x,
lineOrigin.y + glyph.anchor.y));
}
}
}
}
void TextLayout::createLayout (const AttributedString& text, float maxWidth)
{
lines.clear();
width = maxWidth;
justification = text.getJustification();
if (! createNativeLayout (text))
createStandardLayout (text);
recalculateWidth();
}
//==============================================================================
void TextLayout::clear()
namespace TextLayoutHelpers
{
tokens.clear();
totalLines = 0;
}
bool TextLayout::isEmpty() const
{
return tokens.size() == 0;
}
void TextLayout::appendText (const String& text, const Font& font)
{
String::CharPointerType t (text.getCharPointer());
String currentString;
int lastCharType = 0;
for (;;)
struct FontAndColour
{
const juce_wchar c = t.getAndAdvance();
if (c == 0)
break;
FontAndColour (const Font* font_) noexcept : font (font_), colour (0xff000000) {}
int charType;
if (c == '\r' || c == '\n')
const Font* font;
Colour colour;
bool operator!= (const FontAndColour& other) const noexcept
{
charType = 0;
}
else if (CharacterFunctions::isWhitespace (c))
{
charType = 2;
}
else
{
charType = 1;
return (font != other.font && *font != *other.font) || colour != other.colour;
}
};
if (charType == 0 || charType != lastCharType)
{
if (currentString.isNotEmpty())
{
tokens.add (new Token (currentString, font,
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, lastCharType == 2));
}
void TextLayout::setText (const String& text, const Font& font)
{
clear();
appendText (text, font);
}
//==============================================================================
void TextLayout::layout (int maxWidth,
const Justification& justification,
const bool attemptToBalanceLineLengths)
{
if (attemptToBalanceLineLengths)
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)
{
layout (maxWidth, justification, false);
FontAndColour fontAndColour;
Range<int> range;
};
if (getNumLines() <= 1)
return;
const int lastLineW = getLineWidth (getNumLines() - 1);
const int lastButOneLineW = getLineWidth (getNumLines() - 2);
const float prop = lastLineW / (float) lastButOneLineW;
if (prop > 0.9f)
return;
if (prop > bestLineProportion)
{
bestLineProportion = prop;
bestWidth = maxWidth;
}
maxWidth -= 10;
}
layout (bestWidth, justification, false);
}
else
struct Token
{
int x = 0;
int y = 0;
int h = 0;
totalLines = 0;
int i;
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'))
{}
for (i = 0; i < tokens.size(); ++i)
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)
{
Token* const t = tokens.getUnchecked(i);
t->x = x;
t->y = y;
t->line = totalLines;
x += t->w;
h = jmax (h, t->h);
tokens.ensureStorageAllocated (64);
layout.ensureStorageAllocated (totalLines);
const Token* nextTok = tokens [i + 1];
addTextRuns (text);
if (nextTok == 0)
break;
layoutRuns ((int) layout.getWidth());
if (t->isNewLine || ((! nextTok->isWhitespace) && x + nextTok->w > maxWidth))
int charPosition = 0;
int lineStartPosition = 0;
int runStartPosition = 0;
TextLayout::Line* glyphLine = new TextLayout::Line();
TextLayout::Run* glyphRun = new TextLayout::Run();
for (int i = 0; i < tokens.size(); ++i)
{
// finished a line, so go back and update the heights of the things on it
for (int j = i; j >= 0; --j)
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->glyphs.ensureStorageAllocated (glyphRun->glyphs.size() + newGlyphs.size());
for (int j = 0; j < newGlyphs.size(); ++j)
{
Token* const tok = tokens.getUnchecked(j);
if (charPosition == lineStartPosition)
glyphLine->lineOrigin = tokenPos.translated (0, t->font.getAscent());
if (tok->line == totalLines)
tok->lineHeight = h;
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;
}
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->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)
{
const int totalW = (int) layout.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
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 dx = 0;
if (justification.testFlags (Justification::horizontallyCentred))
dx = (totalW - lineW) / 2;
else if (justification.testFlags (Justification::right))
dx = totalW - lineW;
int charType;
if (c == '\r' || c == '\n')
charType = 0;
else if (CharacterFunctions::isWhitespace (c))
charType = 2;
else
charType = 1;
for (int j = tokens.size(); --j >= 0;)
if (charType == 0 || charType != lastCharType)
{
Token* const t = tokens.getUnchecked(j);
if (currentString.isNotEmpty())
tokens.add (new Token (currentString, font, colour,
lastCharType == 2 || lastCharType == 0));
if (t->line == i)
t->x += dx;
currentString = String::charToString (c);
if (c == '\r' && *t == '\n')
currentString += t.getAndAdvance();
}
else
{
currentString += c;
}
lastCharType = charType;
}
if (currentString.isNotEmpty())
tokens.add (new Token (currentString, font, colour, lastCharType == 2));
}
void layoutRuns (const 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);
};
}
//==============================================================================
int TextLayout::getLineWidth (const int lineNumber) const
void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth)
{
int maxW = 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 (t->line == lineNumber && ! t->isWhitespace)
maxW = jmax (maxW, t->x + t->w);
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 (prop > 0.9f)
return;
if (prop > bestLineProportion)
{
bestLineProportion = prop;
bestWidth = maxWidth;
}
maxWidth -= 10.0f;
}
return maxW;
}
int TextLayout::getWidth() const
{
int maxW = 0;
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;
}
int TextLayout::getHeight() const
{
int maxH = 0;
for (int i = tokens.size(); --i >= 0;)
{
const Token* const t = tokens.getUnchecked(i);
if (! t->isWhitespace)
maxH = jmax (maxH, t->y + t->h);
}
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

View file

@ -30,124 +30,145 @@
#include "../placement/juce_Justification.h"
class Graphics;
//==============================================================================
/**
A laid-out arrangement of text.
A Pre-formatted piece of text, which may contain multiple fonts and colours.
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 TextLayout is created from an AttributedString, and once created can be
quickly drawn into a Graphics context.
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.
@see Font, Graphics::drawFittedText, GlyphArrangement
@see AttributedString
*/
class JUCE_API TextLayout
{
public:
//==============================================================================
/** Creates an empty text layout.
Text can then be appended using the appendText() method.
/** Creates an empty layout.
Having created a TextLayout, you can populate it using createLayout() or
createLayoutWithBalancedLineLengths().
*/
TextLayout();
/** Creates a copy of another layout object. */
TextLayout (const TextLayout& other);
/** Creates a text layout from an initial string and font. */
TextLayout (const String& text, const Font& font);
TextLayout (const TextLayout&);
TextLayout& operator= (const TextLayout&);
#if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
TextLayout (TextLayout&& other) noexcept;
TextLayout& operator= (TextLayout&&) noexcept;
#endif
/** Destructor. */
~TextLayout();
/** Copies another layout onto this one. */
TextLayout& operator= (const TextLayout& layoutToCopy);
//==============================================================================
/** Creates a layout from the given attributed string.
This will replace any data that is currently stored in the layout.
*/
void createLayout (const AttributedString& text, float maxWidth);
/** Creates a layout, attempting to choose a width which results in lines
of a similar length.
This will be slower than the normal createLayout method, but produces a
tidier result.
*/
void createLayoutWithBalancedLineLengths (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;
//==============================================================================
/** Clears the layout, removing all its text. */
void clear();
/** 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;
/** Adds a string to the end of the arrangement.
/** The code number of this glyph. */
int glyphCode;
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.
*/
void appendText (const String& textToAppend,
const Font& fontToUse);
/** The glyph's anchor point - this is relative to the line's origin.
@see TextLayout::Line::lineOrigin
*/
Point<float> anchor;
/** Replaces all the text with a new string.
This is equivalent to calling clear() followed by appendText().
*/
void setText (const String& newText,
const Font& fontToUse);
/** Returns true if the layout has not had any text added yet. */
bool isEmpty() const;
float width;
};
//==============================================================================
/** 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.
*/
void layout (int maximumWidth,
const Justification& justification,
bool attemptToBalanceLineLengths);
/** 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 overall width of the entire text layout. */
int getWidth() const;
/** 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 overall height of the entire text layout. */
int getHeight() const;
/** Returns the X position range which contains all the glyphs in this line. */
Range<float> getLineBoundsX() const noexcept;
/** Returns the total number of lines of text. */
int getNumLines() const { return totalLines; }
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;
/** Returns the width of a particular line of text.
@param lineNumber the line, from 0 to (getNumLines() - 1)
*/
int getLineWidth (int lineNumber) const;
private:
Line& operator= (const Line&);
};
//==============================================================================
/** Renders the text at a specified position using a graphics context.
*/
void draw (Graphics& g, int topLeftX, int topLeftY) const;
/** Returns the maximum width of the content. */
float getWidth() const noexcept { return width; }
/** Renders the text within a specified rectangle using a graphics context.
/** Returns the maximum height of the content. */
float getHeight() const noexcept;
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;
/** 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 layout will take ownership of this line object
and will delete it when it is no longer needed. */
void addLine (Line* line);
/** Pre-allocates space for the specified number of lines. */
void ensureStorageAllocated (int numLinesNeeded);
private:
//==============================================================================
class Token;
friend class OwnedArray <Token>;
OwnedArray <Token> tokens;
int totalLines;
OwnedArray<Line> lines;
float width;
Justification justification;
JUCE_LEAK_DETECTOR (TextLayout);
void createStandardLayout (const AttributedString&);
bool createNativeLayout (const AttributedString&);
void recalculateWidth();
};
#endif // __JUCE_TEXTLAYOUT_JUCEHEADER__

View file

@ -46,7 +46,7 @@
management and layout.
*/
#ifndef JUCE_USE_DIRECTWRITE
#define JUCE_USE_DIRECTWRITE 0
#define JUCE_USE_DIRECTWRITE 1
#endif
#ifndef JUCE_INCLUDE_PNGLIB_CODE

View file

@ -33,6 +33,116 @@
namespace CoreTextTypeLayout
{
CTFontRef createCTFont (const Font& font, float fontSize, bool& needsItalicTransform)
{
CFStringRef cfName = font.getTypefaceName().toCFString();
CTFontRef ctFontRef = CTFontCreateWithName (cfName, fontSize, nullptr);
CFRelease (cfName);
if (ctFontRef != nullptr)
{
if (font.isItalic())
{
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
kCTFontItalicTrait, kCTFontItalicTrait);
if (newFont != nullptr)
{
CFRelease (ctFontRef);
ctFontRef = newFont;
}
else
{
needsItalicTransform = true; // couldn't find a proper italic version, so fake it with a transform..
}
}
if (font.isBold())
{
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
kCTFontBoldTrait, kCTFontBoldTrait);
if (newFont != nullptr)
{
CFRelease (ctFontRef);
ctFontRef = newFont;
}
}
}
return ctFontRef;
}
float getFontHeightToCGSizeFactor (const Font& font)
{
static float factor = 0;
if (factor == 0) // (This factor seems to be a constant for all fonts..)
{
bool needsItalicTransform = false;
CTFontRef tempFont = createCTFont (font, 1024, needsItalicTransform);
CGFontRef cgFontRef = CTFontCopyGraphicsFont (tempFont, nullptr);
const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef));
factor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight;
CGFontRelease (cgFontRef);
CFRelease (tempFont);
}
return factor;
}
//==============================================================================
struct Advances
{
Advances (CTRunRef run, const CFIndex numGlyphs)
: advances (CTRunGetAdvancesPtr (run))
{
if (advances == nullptr)
{
local.malloc (numGlyphs);
CTRunGetAdvances (run, CFRangeMake (0, 0), local);
advances = local;
}
}
const CGSize* advances;
HeapBlock<CGSize> local;
};
struct Glyphs
{
Glyphs (CTRunRef run, const int numGlyphs)
: glyphs (CTRunGetGlyphsPtr (run))
{
if (glyphs == nullptr)
{
local.malloc (numGlyphs);
CTRunGetGlyphs (run, CFRangeMake (0, 0), local);
glyphs = local;
}
}
const CGGlyph* glyphs;
HeapBlock<CGGlyph> local;
};
struct Positions
{
Positions (CTRunRef run, const int numGlyphs)
: points (CTRunGetPositionsPtr (run))
{
if (points == nullptr)
{
local.malloc (numGlyphs);
CTRunGetPositions (run, CFRangeMake (0, 0), local);
points = local;
}
}
const CGPoint* points;
HeapBlock<CGPoint> local;
};
//==============================================================================
CFAttributedStringRef createCFAttributedString (const AttributedString& text)
{
#if JUCE_IOS
@ -58,17 +168,10 @@ namespace CoreTextTypeLayout
if (attr->getFont() != nullptr)
{
// Apply fontHeightToCGSizeFactor to the font size since this is how glyphs are drawn
CTFontRef ctFontRef = CTFontCreateWithName (attr->getFont()->getTypefaceName().toCFString(), 1024, nullptr);
CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr);
CFRelease (ctFontRef);
const int totalHeight = abs (CGFontGetAscent (cgFontRef)) + abs (CGFontGetDescent (cgFontRef));
float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight;
CGFontRelease (cgFontRef);
ctFontRef = CTFontCreateWithName (attr->getFont()->getTypefaceName().toCFString(),
attr->getFont()->getHeight() * fontHeightToCGSizeFactor, nullptr);
const Font& f = *attr->getFont();
bool needsItalicTransform = false;
CTFontRef ctFontRef = createCTFont (f, f.getHeight() * getFontHeightToCGSizeFactor (f),
needsItalicTransform);
CFAttributedStringSetAttribute (attribString, CFRangeMake (range.getStart(), range.getLength()),
kCTFontAttributeName, ctFontRef);
@ -190,7 +293,7 @@ namespace CoreTextTypeLayout
CFRelease (frame);
}
void createLayout (GlyphLayout& glyphLayout, const AttributedString& text)
void createLayout (TextLayout& glyphLayout, const AttributedString& text)
{
CFAttributedStringRef attribString = CoreTextTypeLayout::createCFAttributedString (text);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString (attribString);
@ -228,9 +331,9 @@ namespace CoreTextTypeLayout
CGFloat ascent, descent, leading;
CTLineGetTypographicBounds (line, &ascent, &descent, &leading);
GlyphLayout::Line* const glyphLine = new GlyphLayout::Line (lineStringRange, lineOrigin,
(float) ascent, (float) descent, (float) leading,
(int) numRuns);
TextLayout::Line* const glyphLine = new TextLayout::Line (lineStringRange, lineOrigin,
(float) ascent, (float) descent, (float) leading,
(int) numRuns);
glyphLayout.addLine (glyphLine);
for (CFIndex j = 0; j < numRuns; ++j)
@ -239,10 +342,10 @@ namespace CoreTextTypeLayout
const CFIndex numGlyphs = CTRunGetGlyphCount (run);
const CFRange runStringRange = CTRunGetStringRange (run);
GlyphLayout::Run* const glyphRun = new GlyphLayout::Run (Range<int> ((int) runStringRange.location,
(int) (runStringRange.location + runStringRange.length - 1)),
(int) numGlyphs);
glyphLine->addRun (glyphRun);
TextLayout::Run* const glyphRun = new TextLayout::Run (Range<int> ((int) runStringRange.location,
(int) (runStringRange.location + runStringRange.length - 1)),
(int) numGlyphs);
glyphLine->runs.add (glyphRun);
CFDictionaryRef runAttributes = CTRunGetAttributes (run);
@ -250,18 +353,16 @@ namespace CoreTextTypeLayout
if (CFDictionaryGetValueIfPresent (runAttributes, kCTFontAttributeName, (const void **) &ctRunFont))
{
CFStringRef cfsFontName = CTFontCopyPostScriptName (ctRunFont);
const String fontName (String::fromCFString (cfsFontName));
CTFontRef ctFontRef = CTFontCreateWithName (cfsFontName, 1024, nullptr);
CFRelease (cfsFontName);
CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr);
CFRelease (ctFontRef);
const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef));
const float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight;
CGFontRelease (cgFontRef);
glyphRun->setFont (Font (fontName, CTFontGetSize (ctRunFont) / fontHeightToCGSizeFactor, 0));
glyphRun->font = Font (String::fromCFString (cfsFontName),
CTFontGetSize (ctRunFont) / fontHeightToCGSizeFactor, 0); // XXX bold/italic flags?
CFRelease (cfsFontName);
}
CGColorRef cgRunColor;
@ -270,28 +371,17 @@ namespace CoreTextTypeLayout
{
const CGFloat* const components = CGColorGetComponents (cgRunColor);
glyphRun->setColour (Colour::fromFloatRGBA (components[0], components[1], components[2], components[3]));
glyphRun->colour = Colour::fromFloatRGBA (components[0], components[1], components[2], components[3]);
}
const CGGlyph* glyphsPtr = CTRunGetGlyphsPtr (run);
const CGPoint* posPtr = CTRunGetPositionsPtr (run);
HeapBlock <CGGlyph> glyphBuffer;
HeapBlock <CGPoint> positionBuffer;
if (glyphsPtr == nullptr || posPtr == nullptr)
{
// If we can't get a direct pointer, we have to copy the metrics to get them..
glyphBuffer.malloc (numGlyphs);
glyphsPtr = glyphBuffer;
CTRunGetGlyphs (run, CFRangeMake (0, 0), glyphBuffer);
positionBuffer.malloc (numGlyphs);
posPtr = positionBuffer;
CTRunGetPositions (run, CFRangeMake (0, 0), positionBuffer);
}
const CoreTextTypeLayout::Glyphs glyphs (run, numGlyphs);
const CoreTextTypeLayout::Advances advances (run, numGlyphs);
const CoreTextTypeLayout::Positions positions (run, numGlyphs);
for (CFIndex k = 0; k < numGlyphs; ++k)
glyphRun->addGlyph (new GlyphLayout::Glyph (glyphsPtr[k], Point<float> (posPtr[k].x, posPtr[k].y)));
glyphRun->glyphs.add (TextLayout::Glyph (glyphs.glyphs[k], Point<float> (positions.points[k].x,
positions.points[k].y),
advances.advances[k].width));
}
}
@ -314,41 +404,11 @@ public:
ascent (0.0f),
unitsToHeightScaleFactor (0.0f)
{
CFStringRef cfName = font.getTypefaceName().toCFString();
ctFontRef = CTFontCreateWithName (cfName, 1024, nullptr);
CFRelease (cfName);
bool needsItalicTransform = false;
ctFontRef = CoreTextTypeLayout::createCTFont (font, 1024.0f, needsItalicTransform);
if (ctFontRef != nullptr)
{
bool needsItalicTransform = false;
if (font.isItalic())
{
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
kCTFontItalicTrait, kCTFontItalicTrait);
if (newFont != nullptr)
{
CFRelease (ctFontRef);
ctFontRef = newFont;
}
else
{
needsItalicTransform = true; // couldn't find a proper italic version, so fake it with a transform..
}
}
if (font.isBold())
{
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
kCTFontBoldTrait, kCTFontBoldTrait);
if (newFont != nullptr)
{
CFRelease (ctFontRef);
ctFontRef = newFont;
}
}
ascent = std::abs ((float) CTFontGetAscent (ctFontRef));
const float totalSize = ascent + std::abs ((float) CTFontGetDescent (ctFontRef));
ascent /= totalSize;
@ -411,11 +471,11 @@ public:
{
CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i);
CFIndex length = CTRunGetGlyphCount (run);
HeapBlock <CGSize> advances (length);
CTRunGetAdvances (run, CFRangeMake (0, 0), advances);
const CoreTextTypeLayout::Advances advances (run, length);
for (int j = 0; j < length; ++j)
x += (float) advances[j].width;
x += (float) advances.advances[j].width;
}
CFRelease (line);
@ -446,16 +506,15 @@ public:
{
CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i);
CFIndex length = CTRunGetGlyphCount (run);
HeapBlock <CGSize> advances (length);
CTRunGetAdvances (run, CFRangeMake (0, 0), advances);
HeapBlock <CGGlyph> glyphs (length);
CTRunGetGlyphs (run, CFRangeMake (0, 0), glyphs);
const CoreTextTypeLayout::Advances advances (run, length);
const CoreTextTypeLayout::Glyphs glyphs (run, length);
for (int j = 0; j < length; ++j)
{
x += (float) advances[j].width;
x += (float) advances.advances[j].width;
xOffsets.add (x * unitsToHeightScaleFactor);
resultGlyphs.add (glyphs[j]);
resultGlyphs.add (glyphs.glyphs[j]);
}
}
@ -1048,7 +1107,7 @@ Typeface::Ptr Font::getDefaultTypefaceForFont (const Font& font)
return Typeface::createSystemTypefaceFor (f);
}
bool GlyphLayout::createNativeLayout (const AttributedString& text)
bool TextLayout::createNativeLayout (const AttributedString& text)
{
#if JUCE_CORETEXT_AVAILABLE
CoreTextTypeLayout::createLayout (*this, text);

View file

@ -65,7 +65,7 @@ namespace DirectWriteTypeLayout
DWRITE_GLYPH_RUN const* glyphRun, DWRITE_GLYPH_RUN_DESCRIPTION const* runDescription,
IUnknown* clientDrawingEffect)
{
GlyphLayout* const glyphLayout = static_cast<GlyphLayout*> (clientDrawingContext);
TextLayout* const layout = static_cast<TextLayout*> (clientDrawingContext);
if (baselineOriginY != lastOriginY)
{
@ -73,49 +73,52 @@ namespace DirectWriteTypeLayout
++currentLine;
// The x value is only correct when dealing with LTR text
glyphLayout->getLine (currentLine).setLineOrigin (Point<float> (baselineOriginX, baselineOriginY));
layout->getLine (currentLine).lineOrigin = Point<float> (baselineOriginX, baselineOriginY);
}
if (currentLine < 0)
return S_OK;
GlyphLayout::Line& glyphLine = glyphLayout->getLine (currentLine);
TextLayout::Line& glyphLine = layout->getLine (currentLine);
DWRITE_FONT_METRICS dwFontMetrics;
glyphRun->fontFace->GetMetrics (&dwFontMetrics);
const float ascent = scaledFontSize (dwFontMetrics.ascent, dwFontMetrics, glyphRun);
const float descent = scaledFontSize (dwFontMetrics.descent, dwFontMetrics, glyphRun);
glyphLine.increaseAscentDescent (ascent, descent);
glyphLine.ascent = jmax (glyphLine->ascent, scaledFontSize (dwFontMetrics.ascent, dwFontMetrics, glyphRun));
glyphLine.descent = jmax (glyphLine->descent, scaledFontSize (dwFontMetrics.descent, dwFontMetrics, glyphRun));
int styleFlags = 0;
const String fontName (getFontName (glyphRun, styleFlags));
GlyphLayout::Run* const glyphRunLayout = new GlyphLayout::Run (Range<int> (runDescription->textPosition,
runDescription->textPosition + runDescription->stringLength),
glyphRun->glyphCount);
glyphLine.addRun (glyphRunLayout);
TextLayout::Run* const glyphRunLayout = new TextLayout::Run (Range<int> (runDescription->textPosition,
runDescription->textPosition + runDescription->stringLength),
glyphRun->glyphCount);
glyphLine.runs.add (glyphRunLayout);
glyphRun->fontFace->GetMetrics (&dwFontMetrics);
const float totalHeight = std::abs ((float) dwFontMetrics.ascent) + std::abs ((float) dwFontMetrics.descent);
const float fontHeightToEmSizeFactor = (float) dwFontMetrics.designUnitsPerEm / totalHeight;
glyphRunLayout->setFont (Font (fontName, glyphRun->fontEmSize / fontHeightToEmSizeFactor, styleFlags));
glyphRunLayout->setColour (getColourOf (static_cast<ID2D1SolidColorBrush*> (clientDrawingEffect)));
glyphRunLayout->font = Font (fontName, glyphRun->fontEmSize / fontHeightToEmSizeFactor, styleFlags);
glyphRunLayout->colour = getColourOf (static_cast<ID2D1SolidColorBrush*> (clientDrawingEffect));
const Point<float> lineOrigin (glyphLayout->getLine (currentLine).getLineOrigin());
const Point<float> lineOrigin (layout->getLine (currentLine).lineOrigin);
float x = baselineOriginX - lineOrigin.x;
for (UINT32 i = 0; i < glyphRun->glyphCount; ++i)
{
if ((glyphRun->bidiLevel & 1) != 0)
x -= glyphRun->glyphAdvances[i]; // RTL text
const float advance = glyphRun->glyphAdvances[i];
glyphRunLayout->addGlyph (new GlyphLayout::Glyph (glyphRun->glyphIndices[i], Point<float> (x, baselineOriginY - lineOrigin.y)));
if ((glyphRun->bidiLevel & 1) != 0)
x -= advance; // RTL text
glyphRunLayout->glyphs.add (TextLayout::Glyph (glyphRun->glyphIndices[i],
Point<float> (x, baselineOriginY - lineOrigin.y),
advance));
if ((glyphRun->bidiLevel & 1) == 0)
x += glyphRun->glyphAdvances[i]; // LTR text
x += advance; // LTR text
}
return S_OK;
@ -264,7 +267,7 @@ namespace DirectWriteTypeLayout
}
}
void createLayout (GlyphLayout& glyphLayout, const AttributedString& text, IDWriteFactory* const directWriteFactory,
void createLayout (TextLayout& layout, const AttributedString& text, IDWriteFactory* const directWriteFactory,
ID2D1Factory* const direct2dFactory, IDWriteFontCollection* const fontCollection)
{
// To add color to text, we need to create a D2D render target
@ -296,7 +299,7 @@ namespace DirectWriteTypeLayout
ComSmartPtr<IDWriteTextLayout> dwTextLayout;
hr = directWriteFactory->CreateTextLayout (text.getText().toWideCharPointer(), textLen,
dwTextFormat, glyphLayout.getWidth(),
dwTextFormat, layout.getWidth(),
1.0e7f, dwTextLayout.resetAndGetPointerAddress());
const int numAttributes = text.getNumAttributes();
@ -307,7 +310,7 @@ namespace DirectWriteTypeLayout
UINT32 actualLineCount = 0;
hr = dwTextLayout->GetLineMetrics (nullptr, 0, &actualLineCount);
glyphLayout.ensureStorageAllocated (actualLineCount);
layout.ensureStorageAllocated (actualLineCount);
HeapBlock <DWRITE_LINE_METRICS> dwLineMetrics (actualLineCount);
hr = dwTextLayout->GetLineMetrics (dwLineMetrics, actualLineCount, &actualLineCount);
@ -318,19 +321,19 @@ namespace DirectWriteTypeLayout
const Range<int> lineStringRange (lastLocation, (int) lastLocation + dwLineMetrics[i].length);
lastLocation = dwLineMetrics[i].length;
GlyphLayout::Line* glyphLine = new GlyphLayout::Line();
glyphLayout.addLine (glyphLine);
glyphLine->setStringRange (lineStringRange);
TextLayout::Line* glyphLine = new TextLayout::Line();
layout.addLine (glyphLine);
glyphLine->stringRange = lineStringRange;
}
ComSmartPtr<CustomDirectWriteTextRenderer> textRenderer (new CustomDirectWriteTextRenderer (fontCollection));
hr = dwTextLayout->Draw (&glyphLayout, textRenderer, 0, 0);
hr = dwTextLayout->Draw (&layout, textRenderer, 0, 0);
}
}
#endif
bool GlyphLayout::createNativeLayout (const AttributedString& text)
bool TextLayout::createNativeLayout (const AttributedString& text)
{
#if JUCE_USE_DIRECTWRITE
const Direct2DFactories& factories = Direct2DFactories::getInstance();

View file

@ -80,10 +80,10 @@ namespace LookAndFeelHelpers
p.closeSubPath();
}
const Colour createBaseColour (const Colour& buttonColour,
const bool hasKeyboardFocus,
const bool isMouseOverButton,
const bool isButtonDown) noexcept
Colour createBaseColour (const Colour& buttonColour,
const bool hasKeyboardFocus,
const bool isMouseOverButton,
const bool isButtonDown) noexcept
{
const float sat = hasKeyboardFocus ? 1.3f : 0.9f;
const Colour baseColour (buttonColour.withMultipliedSaturation (sat));
@ -96,15 +96,17 @@ namespace LookAndFeelHelpers
return baseColour;
}
const TextLayout layoutTooltipText (const String& text) noexcept
TextLayout layoutTooltipText (const String& text) noexcept
{
const float tooltipFontSize = 12.0f;
const float tooltipFontSize = 13.0f;
const int maxToolTipWidth = 400;
const Font f (tooltipFontSize, Font::bold);
TextLayout tl (text, f);
tl.layout (maxToolTipWidth, Justification::left, true);
AttributedString s;
s.setJustification (Justification::centred);
s.append (text, Font (tooltipFontSize, Font::bold));
TextLayout tl;
tl.createLayoutWithBalancedLineLengths (s, (float) maxToolTipWidth);
return tl;
}
}
@ -527,7 +529,6 @@ void LookAndFeel::drawAlertBox (Graphics& g,
g.fillAll (alert.findColour (AlertWindow::backgroundColourId));
int iconSpaceUsed = 0;
Justification alignment (Justification::horizontallyCentred);
const int iconWidth = 80;
int iconSize = jmin (iconWidth + 50, alert.getHeight() + 20);
@ -577,15 +578,14 @@ void LookAndFeel::drawAlertBox (Graphics& g,
g.fillPath (icon);
iconSpaceUsed = iconWidth;
alignment = Justification::left;
}
g.setColour (alert.findColour (AlertWindow::textColourId));
textLayout.drawWithin (g,
textArea.getX() + iconSpaceUsed, textArea.getY(),
textArea.getWidth() - iconSpaceUsed, textArea.getHeight(),
alignment.getFlags() | Justification::top);
textLayout.draw (g, Rectangle<int> (textArea.getX() + iconSpaceUsed,
textArea.getY(),
textArea.getWidth() - iconSpaceUsed,
textArea.getHeight()).toFloat());
g.setColour (alert.findColour (AlertWindow::outlineColourId));
g.drawRect (0, 0, alert.getWidth(), alert.getHeight());
@ -1657,8 +1657,8 @@ void LookAndFeel::getTooltipSize (const String& tipText, int& width, int& height
{
const TextLayout tl (LookAndFeelHelpers::layoutTooltipText (tipText));
width = tl.getWidth() + 14;
height = tl.getHeight() + 6;
width = (int) (tl.getWidth() + 14.0f);
height = (int) (tl.getHeight() + 6.0f);
}
void LookAndFeel::drawTooltip (Graphics& g, const String& text, int width, int height)
@ -1673,7 +1673,7 @@ void LookAndFeel::drawTooltip (Graphics& g, const String& text, int width, int h
const TextLayout tl (LookAndFeelHelpers::layoutTooltipText (text));
g.setColour (findColour (TooltipWindow::textColourId));
tl.drawWithin (g, 0, 0, width, height, Justification::centred);
tl.draw (g, Rectangle<float> (0.0f, 0.0f, (float) width, (float) height));
}
//==============================================================================

View file

@ -117,10 +117,10 @@ void AlertWindow::setMessage (const String& message)
font = getLookAndFeel().getAlertWindowMessageFont();
Font titleFont (font.getHeight() * 1.1f, Font::bold);
textLayout.setText (getName() + "\n\n", titleFont);
textLayout.appendText (text, font);
AttributedString newText;
newText.append (getName() + "\n\n", Font (font.getHeight() * 1.1f, Font::bold));
newText.append (text, font);
attributedText = newText;
updateLayout (true);
repaint();
@ -271,10 +271,13 @@ public:
void updateLayout (const int width)
{
AttributedString s;
s.setJustification (Justification::topLeft);
s.append (getName(), getFont());
TextLayout text;
text.appendText (getText(), getFont());
text.layout (width - 8, Justification::topLeft, true);
setSize (width, jmin (width, text.getHeight() + (int) getFont().getHeight()));
text.createLayoutWithBalancedLineLengths (s, width - 8.0f);
setSize (width, jmin (width, (int) (text.getHeight() + getFont().getHeight())));
}
private:
@ -399,18 +402,20 @@ void AlertWindow::updateLayout (const bool onlyIncreaseSize)
if (alertIconType == NoIcon)
{
textLayout.layout (w, Justification::horizontallyCentred, true);
attributedText.setJustification (Justification::centredTop);
textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
}
else
{
textLayout.layout (w, Justification::left, true);
attributedText.setJustification (Justification::topLeft);
textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
iconSpace = iconWidth;
}
w = jmax (350, textLayout.getWidth() + iconSpace + edgeGap * 4);
w = jmax (350, (int) textLayout.getWidth() + iconSpace + edgeGap * 4);
w = jmin (w, (int) (getParentWidth() * 0.7f));
const int textLayoutH = textLayout.getHeight();
const int textLayoutH = (int) textLayout.getHeight();
const int textBottom = 16 + titleH + textLayoutH;
int h = textBottom;

View file

@ -446,6 +446,7 @@ protected:
private:
//==============================================================================
String text;
AttributedString attributedText;
TextLayout textLayout;
AlertIconType alertIconType;
ComponentBoundsConstrainer constrainer;

View file

@ -43,12 +43,8 @@ void BubbleMessageComponent::showAt (int x, int y,
const bool removeWhenMouseClicked,
const bool deleteSelfAfterUse)
{
textLayout.clear();
textLayout.setText (text, Font (14.0f));
textLayout.layout (256, Justification::centredLeft, true);
createLayout (text);
setPosition (x, y);
init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse);
}
@ -58,15 +54,20 @@ void BubbleMessageComponent::showAt (Component* const component,
const bool removeWhenMouseClicked,
const bool deleteSelfAfterUse)
{
textLayout.clear();
textLayout.setText (text, Font (14.0f));
textLayout.layout (256, Justification::centredLeft, true);
createLayout (text);
setPosition (component);
init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse);
}
void BubbleMessageComponent::createLayout (const String& text)
{
AttributedString attString;
attString.append (text, Font (14.0f));
attString.setJustification (Justification::centred);
textLayout.createLayoutWithBalancedLineLengths (attString, 256);
}
void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving,
const bool removeWhenMouseClicked,
const bool deleteSelfAfterUse)
@ -92,15 +93,15 @@ void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving,
void BubbleMessageComponent::getContentSize (int& w, int& h)
{
w = textLayout.getWidth() + 16;
h = textLayout.getHeight() + 16;
w = (int) (textLayout.getWidth() + 16.0f);
h = (int) (textLayout.getHeight() + 16.0f);
}
void BubbleMessageComponent::paintContent (Graphics& g, int w, int h)
{
g.setColour (findColour (TooltipWindow::textColourId));
textLayout.drawWithin (g, 0, 0, w, h, Justification::centred);
textLayout.draw (g, Rectangle<float> (0.0f, 0.0f, (float) w, (float) h));
}
void BubbleMessageComponent::timerCallback()

View file

@ -121,6 +121,7 @@ private:
int64 expiryTime;
bool deleteAfterUse;
void createLayout (const String&);
void init (int numMillisecondsBeforeRemoving,
bool removeWhenMouseClicked,
bool deleteSelfAfterUse);