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:
parent
1a5bdda7f1
commit
58db7eb880
16 changed files with 994 additions and 1200 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
|
||||
An attributed string lets you create a string with varied fonts, colours, word-wrapping,
|
||||
layout, etc., and draw it using AttributedString::draw().
|
||||
|
||||
@see TextLayout
|
||||
*/
|
||||
class JUCE_API AttributedString
|
||||
{
|
||||
|
|
@ -44,8 +46,12 @@ public:
|
|||
/** Creates an attributed string with the given text. */
|
||||
explicit AttributedString (const String& text);
|
||||
|
||||
AttributedString (const AttributedString& other);
|
||||
AttributedString& operator= (const AttributedString& other);
|
||||
AttributedString (const AttributedString&);
|
||||
AttributedString& operator= (const AttributedString&);
|
||||
#if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
|
||||
AttributedString (AttributedString&&) noexcept;
|
||||
AttributedString& operator= (AttributedString&&) noexcept;
|
||||
#endif
|
||||
|
||||
/** Destructor. */
|
||||
~AttributedString();
|
||||
|
|
@ -54,12 +60,27 @@ public:
|
|||
/** Returns the complete text of this attributed string. */
|
||||
const String& getText() const noexcept { return text; }
|
||||
|
||||
/** Sets the text.
|
||||
This will change the text, but won't affect any of the attributes that have
|
||||
been added.
|
||||
/** Replaces all the text.
|
||||
This will change the text, but won't affect any of the colour or font attributes
|
||||
that have been added.
|
||||
*/
|
||||
void setText (const String& newText);
|
||||
|
||||
/** Appends some text (with a default font and colour). */
|
||||
void append (const String& textToAppend);
|
||||
/** Appends some text, with a specified font, and the default colour (black). */
|
||||
void append (const String& textToAppend, const Font& font);
|
||||
/** Appends some text, with a specified colour, and the default font. */
|
||||
void append (const String& textToAppend, const Colour& colour);
|
||||
/** Appends some text, with a specified font and colour. */
|
||||
void append (const String& textToAppend, const Font& font, const Colour& colour);
|
||||
|
||||
/** Resets the string, clearing all text and attributes.
|
||||
Note that this won't affect global settings like the justification type,
|
||||
word-wrap mode, etc.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
//==============================================================================
|
||||
/** Draws this string within the given area.
|
||||
The layout of the string within the rectangle is controlled by the justification
|
||||
|
|
@ -143,7 +164,7 @@ public:
|
|||
/** If this attribute specifies a colour, this returns it; otherwise it returns nullptr. */
|
||||
const Colour* getColour() const noexcept { return colour; }
|
||||
|
||||
/** The range of characters to which this attribute should be applied. */
|
||||
/** The range of characters to which this attribute will be applied. */
|
||||
const Range<int> range;
|
||||
|
||||
private:
|
||||
|
|
@ -165,9 +186,15 @@ public:
|
|||
/** Adds a colour attribute for the specified range. */
|
||||
void setColour (const Range<int>& range, const Colour& colour);
|
||||
|
||||
/** Removes all existing colour attributes, and applies this colour to the whole string. */
|
||||
void setColour (const Colour& colour);
|
||||
|
||||
/** Adds a font attribute for the specified range. */
|
||||
void setFont (const Range<int>& range, const Font& font);
|
||||
|
||||
/** Removes all existing font attributes, and applies this font to the whole string. */
|
||||
void setFont (const Font& font);
|
||||
|
||||
private:
|
||||
String text;
|
||||
float lineSpacing;
|
||||
|
|
@ -177,142 +204,4 @@ private:
|
|||
OwnedArray<Attribute> attributes;
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
|
||||
*/
|
||||
class JUCE_API GlyphLayout
|
||||
{
|
||||
public:
|
||||
/** Creates an empty layout. */
|
||||
GlyphLayout();
|
||||
|
||||
/** Destructor. */
|
||||
~GlyphLayout();
|
||||
|
||||
//==============================================================================
|
||||
/** Creates a layout from the given attributed string.
|
||||
This will replace any data that is currently stored in the layout.
|
||||
*/
|
||||
void setText (const AttributedString& text, float maxWidth);
|
||||
|
||||
/** Draws the layout within the specified area.
|
||||
The position of the text within the rectangle is controlled by the justification
|
||||
flags set in the original AttributedString that was used to create this layout.
|
||||
*/
|
||||
void draw (Graphics& g, const Rectangle<float>& area) const;
|
||||
|
||||
//==============================================================================
|
||||
/** A positioned glyph. */
|
||||
class JUCE_API Glyph
|
||||
{
|
||||
public:
|
||||
Glyph (int glyphCode, const Point<float>& anchor) noexcept;
|
||||
~Glyph();
|
||||
|
||||
const int glyphCode;
|
||||
const Point<float> anchor;
|
||||
|
||||
private:
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Glyph);
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** A sequence of glyphs. */
|
||||
class JUCE_API Run
|
||||
{
|
||||
public:
|
||||
Run();
|
||||
Run (const Range<int>& range, int numGlyphsToPreallocate);
|
||||
~Run();
|
||||
|
||||
int getNumGlyphs() const noexcept { return glyphs.size(); }
|
||||
const Font& getFont() const noexcept { return font; }
|
||||
const Colour& getColour() const { return colour; }
|
||||
Glyph& getGlyph (int index) const;
|
||||
|
||||
void setStringRange (const Range<int>& newStringRange) noexcept;
|
||||
void setFont (const Font& newFont);
|
||||
void setColour (const Colour& newColour) noexcept;
|
||||
|
||||
void addGlyph (Glyph* glyph);
|
||||
void ensureStorageAllocated (int numGlyphsNeeded);
|
||||
|
||||
private:
|
||||
OwnedArray<Glyph> glyphs;
|
||||
Range<int> stringRange;
|
||||
Font font;
|
||||
Colour colour;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Run);
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** A line containing a sequence of glyph-runs. */
|
||||
class JUCE_API Line
|
||||
{
|
||||
public:
|
||||
Line() noexcept;
|
||||
Line (const Range<int>& stringRange, const Point<float>& lineOrigin,
|
||||
float ascent, float descent, float leading, int numRunsToPreallocate);
|
||||
~Line();
|
||||
|
||||
const Point<float>& getLineOrigin() const noexcept { return lineOrigin; }
|
||||
|
||||
float getAscent() const noexcept { return ascent; }
|
||||
float getDescent() const noexcept { return descent; }
|
||||
float getLeading() const noexcept { return leading; }
|
||||
|
||||
int getNumRuns() const noexcept { return runs.size(); }
|
||||
Run& getRun (int index) const noexcept;
|
||||
|
||||
void setStringRange (const Range<int>& newStringRange) noexcept;
|
||||
void setLineOrigin (const Point<float>& newLineOrigin) noexcept;
|
||||
void setLeading (float newLeading) noexcept;
|
||||
void increaseAscentDescent (float newAscent, float newDescent) noexcept;
|
||||
|
||||
void addRun (Run* run);
|
||||
|
||||
private:
|
||||
OwnedArray<Run> runs;
|
||||
Range<int> stringRange;
|
||||
Point<float> lineOrigin;
|
||||
float ascent, descent, leading;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Line);
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the maximum width of the content. */
|
||||
float getWidth() const noexcept { return width; }
|
||||
|
||||
/** Returns the maximum height of the content. */
|
||||
float getHeight() const noexcept;
|
||||
|
||||
/** Returns the number of lines in the layout. */
|
||||
int getNumLines() const noexcept { return lines.size(); }
|
||||
|
||||
/** Returns one of the lines. */
|
||||
Line& getLine (int index) const;
|
||||
|
||||
/** Adds a line to the layout. The object passed-in will be owned and deleted by the layout
|
||||
when it is no longer needed.
|
||||
*/
|
||||
void addLine (Line* line);
|
||||
|
||||
/** Pre-allocates space for the specified number of lines. */
|
||||
void ensureStorageAllocated (int numLinesNeeded);
|
||||
|
||||
private:
|
||||
OwnedArray<Line> lines;
|
||||
float width;
|
||||
Justification justification;
|
||||
|
||||
void createStandardLayout (const AttributedString&);
|
||||
bool createNativeLayout (const AttributedString&);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GlyphLayout);
|
||||
};
|
||||
|
||||
#endif // __JUCE_ATTRIBUTEDSTRING_JUCEHEADER__
|
||||
|
|
|
|||
|
|
@ -25,351 +25,589 @@
|
|||
|
||||
BEGIN_JUCE_NAMESPACE
|
||||
|
||||
//==============================================================================
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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__
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
management and layout.
|
||||
*/
|
||||
#ifndef JUCE_USE_DIRECTWRITE
|
||||
#define JUCE_USE_DIRECTWRITE 0
|
||||
#define JUCE_USE_DIRECTWRITE 1
|
||||
#endif
|
||||
|
||||
#ifndef JUCE_INCLUDE_PNGLIB_CODE
|
||||
|
|
|
|||
|
|
@ -33,6 +33,116 @@
|
|||
|
||||
namespace CoreTextTypeLayout
|
||||
{
|
||||
CTFontRef createCTFont (const Font& font, float fontSize, bool& needsItalicTransform)
|
||||
{
|
||||
CFStringRef cfName = font.getTypefaceName().toCFString();
|
||||
CTFontRef ctFontRef = CTFontCreateWithName (cfName, fontSize, nullptr);
|
||||
CFRelease (cfName);
|
||||
|
||||
if (ctFontRef != nullptr)
|
||||
{
|
||||
if (font.isItalic())
|
||||
{
|
||||
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
|
||||
kCTFontItalicTrait, kCTFontItalicTrait);
|
||||
|
||||
if (newFont != nullptr)
|
||||
{
|
||||
CFRelease (ctFontRef);
|
||||
ctFontRef = newFont;
|
||||
}
|
||||
else
|
||||
{
|
||||
needsItalicTransform = true; // couldn't find a proper italic version, so fake it with a transform..
|
||||
}
|
||||
}
|
||||
|
||||
if (font.isBold())
|
||||
{
|
||||
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
|
||||
kCTFontBoldTrait, kCTFontBoldTrait);
|
||||
if (newFont != nullptr)
|
||||
{
|
||||
CFRelease (ctFontRef);
|
||||
ctFontRef = newFont;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctFontRef;
|
||||
}
|
||||
|
||||
float getFontHeightToCGSizeFactor (const Font& font)
|
||||
{
|
||||
static float factor = 0;
|
||||
|
||||
if (factor == 0) // (This factor seems to be a constant for all fonts..)
|
||||
{
|
||||
bool needsItalicTransform = false;
|
||||
CTFontRef tempFont = createCTFont (font, 1024, needsItalicTransform);
|
||||
CGFontRef cgFontRef = CTFontCopyGraphicsFont (tempFont, nullptr);
|
||||
const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef));
|
||||
factor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight;
|
||||
CGFontRelease (cgFontRef);
|
||||
CFRelease (tempFont);
|
||||
}
|
||||
|
||||
return factor;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct Advances
|
||||
{
|
||||
Advances (CTRunRef run, const CFIndex numGlyphs)
|
||||
: advances (CTRunGetAdvancesPtr (run))
|
||||
{
|
||||
if (advances == nullptr)
|
||||
{
|
||||
local.malloc (numGlyphs);
|
||||
CTRunGetAdvances (run, CFRangeMake (0, 0), local);
|
||||
advances = local;
|
||||
}
|
||||
}
|
||||
|
||||
const CGSize* advances;
|
||||
HeapBlock<CGSize> local;
|
||||
};
|
||||
|
||||
struct Glyphs
|
||||
{
|
||||
Glyphs (CTRunRef run, const int numGlyphs)
|
||||
: glyphs (CTRunGetGlyphsPtr (run))
|
||||
{
|
||||
if (glyphs == nullptr)
|
||||
{
|
||||
local.malloc (numGlyphs);
|
||||
CTRunGetGlyphs (run, CFRangeMake (0, 0), local);
|
||||
glyphs = local;
|
||||
}
|
||||
}
|
||||
|
||||
const CGGlyph* glyphs;
|
||||
HeapBlock<CGGlyph> local;
|
||||
};
|
||||
|
||||
struct Positions
|
||||
{
|
||||
Positions (CTRunRef run, const int numGlyphs)
|
||||
: points (CTRunGetPositionsPtr (run))
|
||||
{
|
||||
if (points == nullptr)
|
||||
{
|
||||
local.malloc (numGlyphs);
|
||||
CTRunGetPositions (run, CFRangeMake (0, 0), local);
|
||||
points = local;
|
||||
}
|
||||
}
|
||||
|
||||
const CGPoint* points;
|
||||
HeapBlock<CGPoint> local;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
CFAttributedStringRef createCFAttributedString (const AttributedString& text)
|
||||
{
|
||||
#if JUCE_IOS
|
||||
|
|
@ -58,17 +168,10 @@ namespace CoreTextTypeLayout
|
|||
|
||||
if (attr->getFont() != nullptr)
|
||||
{
|
||||
// Apply fontHeightToCGSizeFactor to the font size since this is how glyphs are drawn
|
||||
CTFontRef ctFontRef = CTFontCreateWithName (attr->getFont()->getTypefaceName().toCFString(), 1024, nullptr);
|
||||
CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr);
|
||||
CFRelease (ctFontRef);
|
||||
|
||||
const int totalHeight = abs (CGFontGetAscent (cgFontRef)) + abs (CGFontGetDescent (cgFontRef));
|
||||
float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight;
|
||||
CGFontRelease (cgFontRef);
|
||||
|
||||
ctFontRef = CTFontCreateWithName (attr->getFont()->getTypefaceName().toCFString(),
|
||||
attr->getFont()->getHeight() * fontHeightToCGSizeFactor, nullptr);
|
||||
const Font& f = *attr->getFont();
|
||||
bool needsItalicTransform = false;
|
||||
CTFontRef ctFontRef = createCTFont (f, f.getHeight() * getFontHeightToCGSizeFactor (f),
|
||||
needsItalicTransform);
|
||||
|
||||
CFAttributedStringSetAttribute (attribString, CFRangeMake (range.getStart(), range.getLength()),
|
||||
kCTFontAttributeName, ctFontRef);
|
||||
|
|
@ -190,7 +293,7 @@ namespace CoreTextTypeLayout
|
|||
CFRelease (frame);
|
||||
}
|
||||
|
||||
void createLayout (GlyphLayout& glyphLayout, const AttributedString& text)
|
||||
void createLayout (TextLayout& glyphLayout, const AttributedString& text)
|
||||
{
|
||||
CFAttributedStringRef attribString = CoreTextTypeLayout::createCFAttributedString (text);
|
||||
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString (attribString);
|
||||
|
|
@ -228,9 +331,9 @@ namespace CoreTextTypeLayout
|
|||
CGFloat ascent, descent, leading;
|
||||
CTLineGetTypographicBounds (line, &ascent, &descent, &leading);
|
||||
|
||||
GlyphLayout::Line* const glyphLine = new GlyphLayout::Line (lineStringRange, lineOrigin,
|
||||
(float) ascent, (float) descent, (float) leading,
|
||||
(int) numRuns);
|
||||
TextLayout::Line* const glyphLine = new TextLayout::Line (lineStringRange, lineOrigin,
|
||||
(float) ascent, (float) descent, (float) leading,
|
||||
(int) numRuns);
|
||||
glyphLayout.addLine (glyphLine);
|
||||
|
||||
for (CFIndex j = 0; j < numRuns; ++j)
|
||||
|
|
@ -239,10 +342,10 @@ namespace CoreTextTypeLayout
|
|||
const CFIndex numGlyphs = CTRunGetGlyphCount (run);
|
||||
const CFRange runStringRange = CTRunGetStringRange (run);
|
||||
|
||||
GlyphLayout::Run* const glyphRun = new GlyphLayout::Run (Range<int> ((int) runStringRange.location,
|
||||
(int) (runStringRange.location + runStringRange.length - 1)),
|
||||
(int) numGlyphs);
|
||||
glyphLine->addRun (glyphRun);
|
||||
TextLayout::Run* const glyphRun = new TextLayout::Run (Range<int> ((int) runStringRange.location,
|
||||
(int) (runStringRange.location + runStringRange.length - 1)),
|
||||
(int) numGlyphs);
|
||||
glyphLine->runs.add (glyphRun);
|
||||
|
||||
CFDictionaryRef runAttributes = CTRunGetAttributes (run);
|
||||
|
||||
|
|
@ -250,18 +353,16 @@ namespace CoreTextTypeLayout
|
|||
if (CFDictionaryGetValueIfPresent (runAttributes, kCTFontAttributeName, (const void **) &ctRunFont))
|
||||
{
|
||||
CFStringRef cfsFontName = CTFontCopyPostScriptName (ctRunFont);
|
||||
const String fontName (String::fromCFString (cfsFontName));
|
||||
|
||||
CTFontRef ctFontRef = CTFontCreateWithName (cfsFontName, 1024, nullptr);
|
||||
CFRelease (cfsFontName);
|
||||
CGFontRef cgFontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr);
|
||||
CFRelease (ctFontRef);
|
||||
|
||||
const int totalHeight = std::abs (CGFontGetAscent (cgFontRef)) + std::abs (CGFontGetDescent (cgFontRef));
|
||||
const float fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (cgFontRef) / (float) totalHeight;
|
||||
CGFontRelease (cgFontRef);
|
||||
|
||||
glyphRun->setFont (Font (fontName, CTFontGetSize (ctRunFont) / fontHeightToCGSizeFactor, 0));
|
||||
glyphRun->font = Font (String::fromCFString (cfsFontName),
|
||||
CTFontGetSize (ctRunFont) / fontHeightToCGSizeFactor, 0); // XXX bold/italic flags?
|
||||
CFRelease (cfsFontName);
|
||||
}
|
||||
|
||||
CGColorRef cgRunColor;
|
||||
|
|
@ -270,28 +371,17 @@ namespace CoreTextTypeLayout
|
|||
{
|
||||
const CGFloat* const components = CGColorGetComponents (cgRunColor);
|
||||
|
||||
glyphRun->setColour (Colour::fromFloatRGBA (components[0], components[1], components[2], components[3]));
|
||||
glyphRun->colour = Colour::fromFloatRGBA (components[0], components[1], components[2], components[3]);
|
||||
}
|
||||
|
||||
const CGGlyph* glyphsPtr = CTRunGetGlyphsPtr (run);
|
||||
const CGPoint* posPtr = CTRunGetPositionsPtr (run);
|
||||
HeapBlock <CGGlyph> glyphBuffer;
|
||||
HeapBlock <CGPoint> positionBuffer;
|
||||
|
||||
if (glyphsPtr == nullptr || posPtr == nullptr)
|
||||
{
|
||||
// If we can't get a direct pointer, we have to copy the metrics to get them..
|
||||
glyphBuffer.malloc (numGlyphs);
|
||||
glyphsPtr = glyphBuffer;
|
||||
CTRunGetGlyphs (run, CFRangeMake (0, 0), glyphBuffer);
|
||||
|
||||
positionBuffer.malloc (numGlyphs);
|
||||
posPtr = positionBuffer;
|
||||
CTRunGetPositions (run, CFRangeMake (0, 0), positionBuffer);
|
||||
}
|
||||
const CoreTextTypeLayout::Glyphs glyphs (run, numGlyphs);
|
||||
const CoreTextTypeLayout::Advances advances (run, numGlyphs);
|
||||
const CoreTextTypeLayout::Positions positions (run, numGlyphs);
|
||||
|
||||
for (CFIndex k = 0; k < numGlyphs; ++k)
|
||||
glyphRun->addGlyph (new GlyphLayout::Glyph (glyphsPtr[k], Point<float> (posPtr[k].x, posPtr[k].y)));
|
||||
glyphRun->glyphs.add (TextLayout::Glyph (glyphs.glyphs[k], Point<float> (positions.points[k].x,
|
||||
positions.points[k].y),
|
||||
advances.advances[k].width));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -314,41 +404,11 @@ public:
|
|||
ascent (0.0f),
|
||||
unitsToHeightScaleFactor (0.0f)
|
||||
{
|
||||
CFStringRef cfName = font.getTypefaceName().toCFString();
|
||||
ctFontRef = CTFontCreateWithName (cfName, 1024, nullptr);
|
||||
CFRelease (cfName);
|
||||
bool needsItalicTransform = false;
|
||||
ctFontRef = CoreTextTypeLayout::createCTFont (font, 1024.0f, needsItalicTransform);
|
||||
|
||||
if (ctFontRef != nullptr)
|
||||
{
|
||||
bool needsItalicTransform = false;
|
||||
|
||||
if (font.isItalic())
|
||||
{
|
||||
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
|
||||
kCTFontItalicTrait, kCTFontItalicTrait);
|
||||
|
||||
if (newFont != nullptr)
|
||||
{
|
||||
CFRelease (ctFontRef);
|
||||
ctFontRef = newFont;
|
||||
}
|
||||
else
|
||||
{
|
||||
needsItalicTransform = true; // couldn't find a proper italic version, so fake it with a transform..
|
||||
}
|
||||
}
|
||||
|
||||
if (font.isBold())
|
||||
{
|
||||
CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0f, nullptr,
|
||||
kCTFontBoldTrait, kCTFontBoldTrait);
|
||||
if (newFont != nullptr)
|
||||
{
|
||||
CFRelease (ctFontRef);
|
||||
ctFontRef = newFont;
|
||||
}
|
||||
}
|
||||
|
||||
ascent = std::abs ((float) CTFontGetAscent (ctFontRef));
|
||||
const float totalSize = ascent + std::abs ((float) CTFontGetDescent (ctFontRef));
|
||||
ascent /= totalSize;
|
||||
|
|
@ -411,11 +471,11 @@ public:
|
|||
{
|
||||
CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i);
|
||||
CFIndex length = CTRunGetGlyphCount (run);
|
||||
HeapBlock <CGSize> advances (length);
|
||||
CTRunGetAdvances (run, CFRangeMake (0, 0), advances);
|
||||
|
||||
const CoreTextTypeLayout::Advances advances (run, length);
|
||||
|
||||
for (int j = 0; j < length; ++j)
|
||||
x += (float) advances[j].width;
|
||||
x += (float) advances.advances[j].width;
|
||||
}
|
||||
|
||||
CFRelease (line);
|
||||
|
|
@ -446,16 +506,15 @@ public:
|
|||
{
|
||||
CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i);
|
||||
CFIndex length = CTRunGetGlyphCount (run);
|
||||
HeapBlock <CGSize> advances (length);
|
||||
CTRunGetAdvances (run, CFRangeMake (0, 0), advances);
|
||||
HeapBlock <CGGlyph> glyphs (length);
|
||||
CTRunGetGlyphs (run, CFRangeMake (0, 0), glyphs);
|
||||
|
||||
const CoreTextTypeLayout::Advances advances (run, length);
|
||||
const CoreTextTypeLayout::Glyphs glyphs (run, length);
|
||||
|
||||
for (int j = 0; j < length; ++j)
|
||||
{
|
||||
x += (float) advances[j].width;
|
||||
x += (float) advances.advances[j].width;
|
||||
xOffsets.add (x * unitsToHeightScaleFactor);
|
||||
resultGlyphs.add (glyphs[j]);
|
||||
resultGlyphs.add (glyphs.glyphs[j]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1048,7 +1107,7 @@ Typeface::Ptr Font::getDefaultTypefaceForFont (const Font& font)
|
|||
return Typeface::createSystemTypefaceFor (f);
|
||||
}
|
||||
|
||||
bool GlyphLayout::createNativeLayout (const AttributedString& text)
|
||||
bool TextLayout::createNativeLayout (const AttributedString& text)
|
||||
{
|
||||
#if JUCE_CORETEXT_AVAILABLE
|
||||
CoreTextTypeLayout::createLayout (*this, text);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ namespace DirectWriteTypeLayout
|
|||
DWRITE_GLYPH_RUN const* glyphRun, DWRITE_GLYPH_RUN_DESCRIPTION const* runDescription,
|
||||
IUnknown* clientDrawingEffect)
|
||||
{
|
||||
GlyphLayout* const glyphLayout = static_cast<GlyphLayout*> (clientDrawingContext);
|
||||
TextLayout* const layout = static_cast<TextLayout*> (clientDrawingContext);
|
||||
|
||||
if (baselineOriginY != lastOriginY)
|
||||
{
|
||||
|
|
@ -73,49 +73,52 @@ namespace DirectWriteTypeLayout
|
|||
++currentLine;
|
||||
|
||||
// The x value is only correct when dealing with LTR text
|
||||
glyphLayout->getLine (currentLine).setLineOrigin (Point<float> (baselineOriginX, baselineOriginY));
|
||||
layout->getLine (currentLine).lineOrigin = Point<float> (baselineOriginX, baselineOriginY);
|
||||
}
|
||||
|
||||
if (currentLine < 0)
|
||||
return S_OK;
|
||||
|
||||
GlyphLayout::Line& glyphLine = glyphLayout->getLine (currentLine);
|
||||
TextLayout::Line& glyphLine = layout->getLine (currentLine);
|
||||
|
||||
DWRITE_FONT_METRICS dwFontMetrics;
|
||||
glyphRun->fontFace->GetMetrics (&dwFontMetrics);
|
||||
|
||||
const float ascent = scaledFontSize (dwFontMetrics.ascent, dwFontMetrics, glyphRun);
|
||||
const float descent = scaledFontSize (dwFontMetrics.descent, dwFontMetrics, glyphRun);
|
||||
glyphLine.increaseAscentDescent (ascent, descent);
|
||||
glyphLine.ascent = jmax (glyphLine->ascent, scaledFontSize (dwFontMetrics.ascent, dwFontMetrics, glyphRun));
|
||||
glyphLine.descent = jmax (glyphLine->descent, scaledFontSize (dwFontMetrics.descent, dwFontMetrics, glyphRun));
|
||||
|
||||
int styleFlags = 0;
|
||||
const String fontName (getFontName (glyphRun, styleFlags));
|
||||
|
||||
GlyphLayout::Run* const glyphRunLayout = new GlyphLayout::Run (Range<int> (runDescription->textPosition,
|
||||
runDescription->textPosition + runDescription->stringLength),
|
||||
glyphRun->glyphCount);
|
||||
glyphLine.addRun (glyphRunLayout);
|
||||
TextLayout::Run* const glyphRunLayout = new TextLayout::Run (Range<int> (runDescription->textPosition,
|
||||
runDescription->textPosition + runDescription->stringLength),
|
||||
glyphRun->glyphCount);
|
||||
glyphLine.runs.add (glyphRunLayout);
|
||||
|
||||
glyphRun->fontFace->GetMetrics (&dwFontMetrics);
|
||||
|
||||
const float totalHeight = std::abs ((float) dwFontMetrics.ascent) + std::abs ((float) dwFontMetrics.descent);
|
||||
const float fontHeightToEmSizeFactor = (float) dwFontMetrics.designUnitsPerEm / totalHeight;
|
||||
|
||||
glyphRunLayout->setFont (Font (fontName, glyphRun->fontEmSize / fontHeightToEmSizeFactor, styleFlags));
|
||||
glyphRunLayout->setColour (getColourOf (static_cast<ID2D1SolidColorBrush*> (clientDrawingEffect)));
|
||||
glyphRunLayout->font = Font (fontName, glyphRun->fontEmSize / fontHeightToEmSizeFactor, styleFlags);
|
||||
glyphRunLayout->colour = getColourOf (static_cast<ID2D1SolidColorBrush*> (clientDrawingEffect));
|
||||
|
||||
const Point<float> lineOrigin (glyphLayout->getLine (currentLine).getLineOrigin());
|
||||
const Point<float> lineOrigin (layout->getLine (currentLine).lineOrigin);
|
||||
float x = baselineOriginX - lineOrigin.x;
|
||||
|
||||
for (UINT32 i = 0; i < glyphRun->glyphCount; ++i)
|
||||
{
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -80,10 +80,10 @@ namespace LookAndFeelHelpers
|
|||
p.closeSubPath();
|
||||
}
|
||||
|
||||
const Colour createBaseColour (const Colour& buttonColour,
|
||||
const bool hasKeyboardFocus,
|
||||
const bool isMouseOverButton,
|
||||
const bool isButtonDown) noexcept
|
||||
Colour createBaseColour (const Colour& buttonColour,
|
||||
const bool hasKeyboardFocus,
|
||||
const bool isMouseOverButton,
|
||||
const bool isButtonDown) noexcept
|
||||
{
|
||||
const float sat = hasKeyboardFocus ? 1.3f : 0.9f;
|
||||
const Colour baseColour (buttonColour.withMultipliedSaturation (sat));
|
||||
|
|
@ -96,15 +96,17 @@ namespace LookAndFeelHelpers
|
|||
return baseColour;
|
||||
}
|
||||
|
||||
const TextLayout layoutTooltipText (const String& text) noexcept
|
||||
TextLayout layoutTooltipText (const String& text) noexcept
|
||||
{
|
||||
const float tooltipFontSize = 12.0f;
|
||||
const float tooltipFontSize = 13.0f;
|
||||
const int maxToolTipWidth = 400;
|
||||
|
||||
const Font f (tooltipFontSize, Font::bold);
|
||||
TextLayout tl (text, f);
|
||||
tl.layout (maxToolTipWidth, Justification::left, true);
|
||||
AttributedString s;
|
||||
s.setJustification (Justification::centred);
|
||||
s.append (text, Font (tooltipFontSize, Font::bold));
|
||||
|
||||
TextLayout tl;
|
||||
tl.createLayoutWithBalancedLineLengths (s, (float) maxToolTipWidth);
|
||||
return tl;
|
||||
}
|
||||
}
|
||||
|
|
@ -527,7 +529,6 @@ void LookAndFeel::drawAlertBox (Graphics& g,
|
|||
g.fillAll (alert.findColour (AlertWindow::backgroundColourId));
|
||||
|
||||
int iconSpaceUsed = 0;
|
||||
Justification alignment (Justification::horizontallyCentred);
|
||||
|
||||
const int iconWidth = 80;
|
||||
int iconSize = jmin (iconWidth + 50, alert.getHeight() + 20);
|
||||
|
|
@ -577,15 +578,14 @@ void LookAndFeel::drawAlertBox (Graphics& g,
|
|||
g.fillPath (icon);
|
||||
|
||||
iconSpaceUsed = iconWidth;
|
||||
alignment = Justification::left;
|
||||
}
|
||||
|
||||
g.setColour (alert.findColour (AlertWindow::textColourId));
|
||||
|
||||
textLayout.drawWithin (g,
|
||||
textArea.getX() + iconSpaceUsed, textArea.getY(),
|
||||
textArea.getWidth() - iconSpaceUsed, textArea.getHeight(),
|
||||
alignment.getFlags() | Justification::top);
|
||||
textLayout.draw (g, Rectangle<int> (textArea.getX() + iconSpaceUsed,
|
||||
textArea.getY(),
|
||||
textArea.getWidth() - iconSpaceUsed,
|
||||
textArea.getHeight()).toFloat());
|
||||
|
||||
g.setColour (alert.findColour (AlertWindow::outlineColourId));
|
||||
g.drawRect (0, 0, alert.getWidth(), alert.getHeight());
|
||||
|
|
@ -1657,8 +1657,8 @@ void LookAndFeel::getTooltipSize (const String& tipText, int& width, int& height
|
|||
{
|
||||
const TextLayout tl (LookAndFeelHelpers::layoutTooltipText (tipText));
|
||||
|
||||
width = tl.getWidth() + 14;
|
||||
height = tl.getHeight() + 6;
|
||||
width = (int) (tl.getWidth() + 14.0f);
|
||||
height = (int) (tl.getHeight() + 6.0f);
|
||||
}
|
||||
|
||||
void LookAndFeel::drawTooltip (Graphics& g, const String& text, int width, int height)
|
||||
|
|
@ -1673,7 +1673,7 @@ void LookAndFeel::drawTooltip (Graphics& g, const String& text, int width, int h
|
|||
const TextLayout tl (LookAndFeelHelpers::layoutTooltipText (text));
|
||||
|
||||
g.setColour (findColour (TooltipWindow::textColourId));
|
||||
tl.drawWithin (g, 0, 0, width, height, Justification::centred);
|
||||
tl.draw (g, Rectangle<float> (0.0f, 0.0f, (float) width, (float) height));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -117,10 +117,10 @@ void AlertWindow::setMessage (const String& message)
|
|||
|
||||
font = getLookAndFeel().getAlertWindowMessageFont();
|
||||
|
||||
Font titleFont (font.getHeight() * 1.1f, Font::bold);
|
||||
textLayout.setText (getName() + "\n\n", titleFont);
|
||||
|
||||
textLayout.appendText (text, font);
|
||||
AttributedString newText;
|
||||
newText.append (getName() + "\n\n", Font (font.getHeight() * 1.1f, Font::bold));
|
||||
newText.append (text, font);
|
||||
attributedText = newText;
|
||||
|
||||
updateLayout (true);
|
||||
repaint();
|
||||
|
|
@ -271,10 +271,13 @@ public:
|
|||
|
||||
void updateLayout (const int width)
|
||||
{
|
||||
AttributedString s;
|
||||
s.setJustification (Justification::topLeft);
|
||||
s.append (getName(), getFont());
|
||||
|
||||
TextLayout text;
|
||||
text.appendText (getText(), getFont());
|
||||
text.layout (width - 8, Justification::topLeft, true);
|
||||
setSize (width, jmin (width, text.getHeight() + (int) getFont().getHeight()));
|
||||
text.createLayoutWithBalancedLineLengths (s, width - 8.0f);
|
||||
setSize (width, jmin (width, (int) (text.getHeight() + getFont().getHeight())));
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
@ -399,18 +402,20 @@ void AlertWindow::updateLayout (const bool onlyIncreaseSize)
|
|||
|
||||
if (alertIconType == NoIcon)
|
||||
{
|
||||
textLayout.layout (w, Justification::horizontallyCentred, true);
|
||||
attributedText.setJustification (Justification::centredTop);
|
||||
textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
|
||||
}
|
||||
else
|
||||
{
|
||||
textLayout.layout (w, Justification::left, true);
|
||||
attributedText.setJustification (Justification::topLeft);
|
||||
textLayout.createLayoutWithBalancedLineLengths (attributedText, (float) w);
|
||||
iconSpace = iconWidth;
|
||||
}
|
||||
|
||||
w = jmax (350, textLayout.getWidth() + iconSpace + edgeGap * 4);
|
||||
w = jmax (350, (int) textLayout.getWidth() + iconSpace + edgeGap * 4);
|
||||
w = jmin (w, (int) (getParentWidth() * 0.7f));
|
||||
|
||||
const int textLayoutH = textLayout.getHeight();
|
||||
const int textLayoutH = (int) textLayout.getHeight();
|
||||
const int textBottom = 16 + titleH + textLayoutH;
|
||||
int h = textBottom;
|
||||
|
||||
|
|
|
|||
|
|
@ -446,6 +446,7 @@ protected:
|
|||
private:
|
||||
//==============================================================================
|
||||
String text;
|
||||
AttributedString attributedText;
|
||||
TextLayout textLayout;
|
||||
AlertIconType alertIconType;
|
||||
ComponentBoundsConstrainer constrainer;
|
||||
|
|
|
|||
|
|
@ -43,12 +43,8 @@ void BubbleMessageComponent::showAt (int x, int y,
|
|||
const bool removeWhenMouseClicked,
|
||||
const bool deleteSelfAfterUse)
|
||||
{
|
||||
textLayout.clear();
|
||||
textLayout.setText (text, Font (14.0f));
|
||||
textLayout.layout (256, Justification::centredLeft, true);
|
||||
|
||||
createLayout (text);
|
||||
setPosition (x, y);
|
||||
|
||||
init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse);
|
||||
}
|
||||
|
||||
|
|
@ -58,15 +54,20 @@ void BubbleMessageComponent::showAt (Component* const component,
|
|||
const bool removeWhenMouseClicked,
|
||||
const bool deleteSelfAfterUse)
|
||||
{
|
||||
textLayout.clear();
|
||||
textLayout.setText (text, Font (14.0f));
|
||||
textLayout.layout (256, Justification::centredLeft, true);
|
||||
|
||||
createLayout (text);
|
||||
setPosition (component);
|
||||
|
||||
init (numMillisecondsBeforeRemoving, removeWhenMouseClicked, deleteSelfAfterUse);
|
||||
}
|
||||
|
||||
void BubbleMessageComponent::createLayout (const String& text)
|
||||
{
|
||||
AttributedString attString;
|
||||
attString.append (text, Font (14.0f));
|
||||
attString.setJustification (Justification::centred);
|
||||
|
||||
textLayout.createLayoutWithBalancedLineLengths (attString, 256);
|
||||
}
|
||||
|
||||
void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving,
|
||||
const bool removeWhenMouseClicked,
|
||||
const bool deleteSelfAfterUse)
|
||||
|
|
@ -92,15 +93,15 @@ void BubbleMessageComponent::init (const int numMillisecondsBeforeRemoving,
|
|||
|
||||
void BubbleMessageComponent::getContentSize (int& w, int& h)
|
||||
{
|
||||
w = textLayout.getWidth() + 16;
|
||||
h = textLayout.getHeight() + 16;
|
||||
w = (int) (textLayout.getWidth() + 16.0f);
|
||||
h = (int) (textLayout.getHeight() + 16.0f);
|
||||
}
|
||||
|
||||
void BubbleMessageComponent::paintContent (Graphics& g, int w, int h)
|
||||
{
|
||||
g.setColour (findColour (TooltipWindow::textColourId));
|
||||
|
||||
textLayout.drawWithin (g, 0, 0, w, h, Justification::centred);
|
||||
textLayout.draw (g, Rectangle<float> (0.0f, 0.0f, (float) w, (float) h));
|
||||
}
|
||||
|
||||
void BubbleMessageComponent::timerCallback()
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ private:
|
|||
int64 expiryTime;
|
||||
bool deleteAfterUse;
|
||||
|
||||
void createLayout (const String&);
|
||||
void init (int numMillisecondsBeforeRemoving,
|
||||
bool removeWhenMouseClicked,
|
||||
bool deleteSelfAfterUse);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue