29static constexpr bool isNonBreakingSpace (
const juce_wchar c)
37PositionedGlyph::PositionedGlyph() noexcept
38 : character (0), glyph (0), x (0), y (0), w (0), whitespace (
false)
49static void drawGlyphWithFont (Graphics& g,
int glyph,
const Font& font, AffineTransform t)
51 auto& context = g.getInternalContext();
52 context.setFont (font);
53 context.drawGlyph (glyph, t);
59 drawGlyphWithFont (g, glyph, font, AffineTransform::translation (x, y));
65 drawGlyphWithFont (g, glyph, font, AffineTransform::translation (x, y).followedBy (transform));
68void PositionedGlyph::createPath (
Path& path)
const
72 if (
auto t = font.getTypefacePtr())
75 t->getOutlineForGlyph (glyph, p);
77 path.
addPath (p, AffineTransform::scale (font.getHeight() * font.getHorizontalScale(), font.getHeight())
83bool PositionedGlyph::hitTest (
float px,
float py)
const
85 if (getBounds().contains (px, py) && ! isWhitespace())
87 if (
auto t = font.getTypefacePtr())
90 t->getOutlineForGlyph (glyph, p);
92 AffineTransform::translation (-x, -y)
93 .scaled (1.0f / (font.getHeight() * font.getHorizontalScale()), 1.0f / font.getHeight())
94 .transformPoint (px, py);
103void PositionedGlyph::moveBy (
float deltaX,
float deltaY)
111GlyphArrangement::GlyphArrangement()
113 glyphs.ensureStorageAllocated (128);
117void GlyphArrangement::clear()
124 return glyphs.getReference (index);
130 glyphs.addArray (other.glyphs);
138void GlyphArrangement::removeRangeOfGlyphs (
int startIndex,
int num)
140 glyphs.removeRange (startIndex, num < 0 ? glyphs.size() : num);
144void GlyphArrangement::addLineOfText (
const Font& font,
const String& text,
float xOffset,
float yOffset)
146 addCurtailedLineOfText (font, text, xOffset, yOffset, 1.0e10f,
false);
149void GlyphArrangement::addCurtailedLineOfText (
const Font& font,
const String& text,
150 float xOffset,
float yOffset,
151 float maxWidthPixels,
bool useEllipsis)
158 auto textLen = newGlyphs.
size();
159 glyphs.ensureStorageAllocated (glyphs.size() + textLen);
163 for (
int i = 0; i < textLen; ++i)
167 if (nextX > maxWidthPixels + 1.0f)
170 if (useEllipsis && textLen > 3 && glyphs.size() >= 3)
171 insertEllipsis (font, xOffset + maxWidthPixels, 0, glyphs.size());
177 auto isWhitespace = isNonBreakingSpace (*t) || t.isWhitespace();
181 xOffset + thisX, yOffset,
182 nextX - thisX, isWhitespace));
187int GlyphArrangement::insertEllipsis (
const Font& font,
float maxXPos,
int startIndex,
int endIndex)
191 if (! glyphs.isEmpty())
198 float xOffset = 0.0f, yOffset = 0.0f;
200 while (endIndex > startIndex)
206 glyphs.remove (endIndex);
209 if (xOffset + dx * 3 <= maxXPos)
213 for (
int i = 3; --i >= 0;)
215 glyphs.insert (endIndex++, PositionedGlyph (font,
'.', dotGlyphs.
getFirst(),
216 xOffset, yOffset, dx,
false));
220 if (xOffset > maxXPos)
228void GlyphArrangement::addJustifiedText (
const Font& font,
const String& text,
229 float x,
float y,
float maxLineWidth,
233 auto lineStartIndex = glyphs.size();
234 addLineOfText (font, text, x, y);
238 while (lineStartIndex < glyphs.size())
240 int i = lineStartIndex;
242 if (glyphs.getReference (i).getCharacter() !=
'\n'
243 && glyphs.getReference (i).getCharacter() !=
'\r')
246 auto lineMaxX = glyphs.getReference (lineStartIndex).getLeft() + maxLineWidth;
247 int lastWordBreakIndex = -1;
249 while (i < glyphs.size())
251 auto& pg = glyphs.getReference (i);
252 auto c = pg.getCharacter();
254 if (c ==
'\r' || c ==
'\n')
258 if (c ==
'\r' && i < glyphs.size()
259 && glyphs.getReference (i).getCharacter() ==
'\n')
265 if (pg.isWhitespace())
267 lastWordBreakIndex = i + 1;
269 else if (pg.getRight() - 0.0001f >= lineMaxX)
271 if (lastWordBreakIndex >= 0)
272 i = lastWordBreakIndex;
280 auto currentLineStartX = glyphs.getReference (lineStartIndex).getLeft();
281 auto currentLineEndX = currentLineStartX;
283 for (
int j = i; --j >= lineStartIndex;)
285 if (! glyphs.getReference (j).isWhitespace())
287 currentLineEndX = glyphs.getReference (j).getRight();
294 if (horizontalLayout.
testFlags (Justification::horizontallyJustified))
295 spreadOutLine (lineStartIndex, i - lineStartIndex, maxLineWidth);
296 else if (horizontalLayout.
testFlags (Justification::horizontallyCentred))
297 deltaX = (maxLineWidth - (currentLineEndX - currentLineStartX)) * 0.5f;
298 else if (horizontalLayout.
testFlags (Justification::right))
299 deltaX = maxLineWidth - (currentLineEndX - currentLineStartX);
301 moveRangeOfGlyphs (lineStartIndex, i - lineStartIndex,
302 x + deltaX - currentLineStartX, y - originalY);
310void GlyphArrangement::addFittedText (
const Font& f,
const String& text,
311 float x,
float y,
float width,
float height,
313 float minimumHorizontalScale)
316 minimumHorizontalScale = Font::getDefaultMinimumHorizontalScaleFactor();
319 jassert (minimumHorizontalScale > 0 && minimumHorizontalScale <= 1.0f);
323 addLinesWithLineBreaks (text, f, x, y, width, height, layout);
327 auto startIndex = glyphs.size();
328 auto trimmed = text.
trim();
329 addLineOfText (f, trimmed, x, y);
330 auto numGlyphs = glyphs.size() - startIndex;
334 auto lineWidth = glyphs.getReference (glyphs.size() - 1).getRight()
335 - glyphs.getReference (startIndex).getLeft();
339 if (lineWidth * minimumHorizontalScale < width)
341 if (lineWidth > width)
342 stretchRangeOfGlyphs (startIndex, numGlyphs, width / lineWidth);
344 justifyGlyphs (startIndex, numGlyphs, x, y, width, height, layout);
346 else if (maximumLines <= 1)
348 fitLineIntoSpace (startIndex, numGlyphs, x, y, width, height,
349 f, layout, minimumHorizontalScale);
353 splitLines (trimmed, f, startIndex, x, y, width, height,
354 maximumLines, lineWidth, layout, minimumHorizontalScale);
362void GlyphArrangement::moveRangeOfGlyphs (
int startIndex,
int num,
const float dx,
const float dy)
368 if (num < 0 || startIndex + num > glyphs.size())
369 num = glyphs.size() - startIndex;
372 glyphs.getReference (startIndex++).moveBy (dx, dy);
376void GlyphArrangement::addLinesWithLineBreaks (
const String& text,
const Font& f,
377 float x,
float y,
float width,
float height,
Justification layout)
383 auto dy = y - bb.
getY();
385 if (layout.
testFlags (Justification::verticallyCentred)) dy += (height - bb.getHeight()) * 0.5f;
386 else if (layout.
testFlags (Justification::bottom)) dy += (height - bb.getHeight());
390 glyphs.addArray (ga.glyphs);
393int GlyphArrangement::fitLineIntoSpace (
int start,
int numGlyphs,
float x,
float y,
float w,
float h,
const Font& font,
394 Justification justification,
float minimumHorizontalScale)
397 auto lineStartX = glyphs.getReference (start).getLeft();
398 auto lineWidth = glyphs.getReference (start + numGlyphs - 1).getRight() - lineStartX;
402 if (minimumHorizontalScale < 1.0f)
404 stretchRangeOfGlyphs (start, numGlyphs, jmax (minimumHorizontalScale, w / lineWidth));
405 lineWidth = glyphs.getReference (start + numGlyphs - 1).getRight() - lineStartX - 0.5f;
410 numDeleted = insertEllipsis (font, lineStartX + w, start, start + numGlyphs);
411 numGlyphs -= numDeleted;
415 justifyGlyphs (start, numGlyphs, x, y, w, h, justification);
419void GlyphArrangement::stretchRangeOfGlyphs (
int startIndex,
int num,
float horizontalScaleFactor)
423 if (num < 0 || startIndex + num > glyphs.size())
424 num = glyphs.size() - startIndex;
428 auto xAnchor = glyphs.getReference (startIndex).getLeft();
432 auto& pg = glyphs.getReference (startIndex++);
434 pg.x = xAnchor + (pg.x - xAnchor) * horizontalScaleFactor;
435 pg.font.setHorizontalScale (pg.font.getHorizontalScale() * horizontalScaleFactor);
436 pg.w *= horizontalScaleFactor;
441Rectangle<float> GlyphArrangement::getBoundingBox (
int startIndex,
int num,
bool includeWhitespace)
const
445 if (num < 0 || startIndex + num > glyphs.size())
446 num = glyphs.size() - startIndex;
452 auto& pg = glyphs.getReference (startIndex++);
454 if (includeWhitespace || ! pg.isWhitespace())
455 result = result.
getUnion (pg.getBounds());
461void GlyphArrangement::justifyGlyphs (
int startIndex,
int num,
462 float x,
float y,
float width,
float height,
465 jassert (num >= 0 && startIndex >= 0);
467 if (glyphs.size() > 0 && num > 0)
469 auto bb = getBoundingBox (startIndex, num, ! justification.
testFlags (Justification::horizontallyJustified
470 | Justification::horizontallyCentred));
471 float deltaX = x, deltaY = y;
473 if (justification.
testFlags (Justification::horizontallyJustified)) deltaX -= bb.getX();
474 else if (justification.
testFlags (Justification::horizontallyCentred)) deltaX += (width - bb.getWidth()) * 0.5f - bb.getX();
475 else if (justification.
testFlags (Justification::right)) deltaX += width - bb.getRight();
476 else deltaX -= bb.getX();
478 if (justification.
testFlags (Justification::top)) deltaY -= bb.getY();
479 else if (justification.
testFlags (Justification::bottom)) deltaY += height - bb.getBottom();
480 else deltaY += (height - bb.getHeight()) * 0.5f - bb.getY();
482 moveRangeOfGlyphs (startIndex, num, deltaX, deltaY);
484 if (justification.
testFlags (Justification::horizontallyJustified))
487 auto baseY = glyphs.getReference (startIndex).getBaselineY();
490 for (i = 0; i < num; ++i)
492 auto glyphY = glyphs.getReference (startIndex + i).getBaselineY();
496 spreadOutLine (startIndex + lineStart, i - lineStart, width);
504 spreadOutLine (startIndex + lineStart, i - lineStart, width);
509void GlyphArrangement::spreadOutLine (
int start,
int num,
float targetWidth)
511 if (start + num < glyphs.size()
512 && glyphs.getReference (start + num - 1).getCharacter() !=
'\r'
513 && glyphs.getReference (start + num - 1).getCharacter() !=
'\n')
518 for (
int i = 0; i < num; ++i)
520 if (glyphs.getReference (start + i).isWhitespace())
531 numSpaces -= spacesAtEnd;
535 auto startX = glyphs.getReference (start).getLeft();
536 auto endX = glyphs.getReference (start + num - 1 - spacesAtEnd).getRight();
538 auto extraPaddingBetweenWords = (targetWidth - (endX - startX)) / (
float) numSpaces;
541 for (
int i = 0; i < num; ++i)
543 glyphs.getReference (start + i).moveBy (deltaX, 0.0f);
545 if (glyphs.getReference (start + i).isWhitespace())
546 deltaX += extraPaddingBetweenWords;
552static bool isBreakableGlyph (
const PositionedGlyph& g)
noexcept
554 return ! isNonBreakingSpace (g.getCharacter()) && (g.isWhitespace() || g.getCharacter() ==
'-');
557void GlyphArrangement::splitLines (
const String& text, Font font,
int startIndex,
558 float x,
float y,
float width,
float height,
int maximumLines,
559 float lineWidth, Justification layout,
float minimumHorizontalScale)
561 auto length = text.length();
562 auto originalStartIndex = startIndex;
565 if (length <= 12 && ! text.containsAnyOf (
" -\t\r\n"))
568 maximumLines =
jmin (maximumLines, length);
570 while (numLines < maximumLines)
573 auto newFontHeight = height / (
float) numLines;
575 if (newFontHeight < font.getHeight())
577 font.setHeight (jmax (8.0f, newFontHeight));
579 removeRangeOfGlyphs (startIndex, -1);
580 addLineOfText (font, text, x, y);
582 lineWidth = glyphs.getReference (glyphs.size() - 1).getRight()
583 - glyphs.getReference (startIndex).getLeft();
588 const float lineLengthUnevennessAllowance = 80.0f;
590 if ((
float) numLines > (lineWidth + lineLengthUnevennessAllowance) / width || newFontHeight < 8.0f)
599 auto widthPerLine =
jmin (width / minimumHorizontalScale,
600 lineWidth / (
float) numLines);
602 while (lineY < y + height)
604 auto endIndex = startIndex;
605 auto lineStartX = glyphs.getReference (startIndex).getLeft();
606 auto lineBottomY = lineY + font.getHeight();
608 if (lineIndex++ >= numLines - 1
609 || lineBottomY >= y + height)
611 widthPerLine = width;
612 endIndex = glyphs.size();
616 while (endIndex < glyphs.size())
618 if (glyphs.getReference (endIndex).getRight() - lineStartX > widthPerLine)
622 auto searchStartIndex = endIndex;
624 while (endIndex < glyphs.size())
626 auto& g = glyphs.getReference (endIndex);
628 if ((g.getRight() - lineStartX) * minimumHorizontalScale < width)
630 if (isBreakableGlyph (g))
639 endIndex = searchStartIndex;
641 for (
int back = 1; back <
jmin (7, endIndex - startIndex - 1); ++back)
643 if (isBreakableGlyph (glyphs.getReference (endIndex - back)))
645 endIndex -= back - 1;
662 auto wsStart = endIndex;
663 auto wsEnd = endIndex;
665 while (wsStart > 0 && glyphs.getReference (wsStart - 1).isWhitespace())
668 while (wsEnd < glyphs.size() && glyphs.getReference (wsEnd).isWhitespace())
671 removeRangeOfGlyphs (wsStart, wsEnd - wsStart);
672 endIndex =
jmax (wsStart, startIndex + 1);
675 endIndex -= fitLineIntoSpace (startIndex, endIndex - startIndex,
676 x, lineY, width, font.getHeight(), font,
677 layout.getOnlyHorizontalFlags() | Justification::verticallyCentred,
678 minimumHorizontalScale);
680 startIndex = endIndex;
683 if (startIndex >= glyphs.size())
687 justifyGlyphs (originalStartIndex, glyphs.size() - originalStartIndex,
688 x, y, width, height, layout.getFlags() & ~Justification::horizontallyJustified);
692void GlyphArrangement::drawGlyphUnderline (
const Graphics& g,
const PositionedGlyph& pg,
693 int i, AffineTransform transform)
const
695 auto lineThickness = (pg.font.getDescent()) * 0.3f;
696 auto nextX = pg.x + pg.w;
698 if (i < glyphs.size() - 1 &&
approximatelyEqual (glyphs.getReference (i + 1).y, pg.y))
699 nextX = glyphs.getReference (i + 1).x;
702 p.addRectangle (pg.x, pg.y + lineThickness * 2.0f, nextX - pg.x, lineThickness);
703 g.fillPath (p, transform);
706void GlyphArrangement::draw (
const Graphics& g)
const
713 auto& context = g.getInternalContext();
714 auto lastFont = context.getFont();
715 bool needToRestore =
false;
717 for (
int i = 0; i < glyphs.size(); ++i)
719 auto& pg = glyphs.getReference (i);
721 if (pg.font.isUnderlined())
722 drawGlyphUnderline (g, pg, i, transform);
724 if (! pg.isWhitespace())
726 if (lastFont != pg.font)
732 needToRestore =
true;
736 context.setFont (lastFont);
739 context.drawGlyph (pg.glyph, AffineTransform::translation (pg.x, pg.y)
740 .followedBy (transform));
745 context.restoreState();
748void GlyphArrangement::createPath (
Path& path)
const
750 for (
auto& g : glyphs)
754int GlyphArrangement::findGlyphIndexAt (
float x,
float y)
const
756 for (
int i = 0; i < glyphs.size(); ++i)
757 if (glyphs.getReference (i).hitTest (x, y))
Holds a resizable array of primitive or copy-by-value objects.
ElementType getUnchecked(int index) const
Returns one of the elements in the array, without checking the index passed in.
int size() const noexcept
Returns the current number of elements in the array.
ElementType getFirst() const noexcept
Returns the first element in the array, or a default value if the array is empty.
ElementType & getReference(int index) noexcept
Returns a direct reference to one of the elements in the array, without checking the index passed in.
Represents a particular font, including its size, style, etc.
void getGlyphPositions(const String &text, Array< int > &glyphs, Array< float > &xOffsets) const
Returns the series of glyph numbers and their x offsets needed to represent a string.
float getHeight() const noexcept
Returns the total height of this font, in pixels.
A set of glyphs, each with a position.
Rectangle< float > getBoundingBox(int startIndex, int numGlyphs, bool includeWhitespace) const
Finds the smallest rectangle that will enclose a subset of the glyphs.
void addJustifiedText(const Font &font, const String &text, float x, float y, float maxLineWidth, Justification horizontalLayout, float leading=0.0f)
Adds some multi-line text, breaking lines at word-boundaries if they are too wide.
void moveRangeOfGlyphs(int startIndex, int numGlyphs, float deltaX, float deltaY)
Shifts a set of glyphs by a given amount.
A graphics context, used for drawing a component or image.
Represents a type of justification to be used when positioning graphical items.
bool testFlags(int flagsToTest) const noexcept
Tests a set of flags for this object.
A path is a sequence of lines and curves that may either form a closed shape or be open-ended.
bool contains(float x, float y, float tolerance=defaultToleranceForTesting) const
Checks whether a point lies within the path.
void addPath(const Path &pathToAppend)
Adds another path to this one.
A glyph from a particular font, with a particular size, style, typeface and position.
Manages a rectangle and allows geometric operations to be performed on it.
Rectangle getUnion(Rectangle other) const noexcept
Returns the smallest rectangle that contains both this one and the one passed-in.
ValueType getY() const noexcept
Returns the y coordinate of the rectangle's top edge.
CharPointerType getCharPointer() const noexcept
Returns the character pointer currently being used to store this string.
String trim() const
Returns a copy of this string with any whitespace characters removed from the start and end.
bool containsAnyOf(StringRef charactersItMightContain) const noexcept
Looks for any of a set of characters in the string.
bool isNotEmpty() const noexcept
Returns true if the string contains at least one character.
wchar_t juce_wchar
A platform-independent 32-bit unicode character type.
constexpr bool approximatelyEqual(Type a, Type b, Tolerance< Type > tolerance=Tolerance< Type >{} .withAbsolute(std::numeric_limits< Type >::min()) .withRelative(std::numeric_limits< Type >::epsilon()))
Returns true if the two floating-point numbers are approximately equal.
constexpr Type jmin(Type a, Type b)
Returns the smaller of two values.
constexpr Type jmax(Type a, Type b)
Returns the larger of two values.
Type unalignedPointerCast(void *ptr) noexcept
Casts a pointer to another type via void*, which suppresses the cast-align warning which sometimes ar...