2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
23 ==============================================================================
29 static constexpr bool isNonBreakingSpace (const juce_wchar c
)
37 PositionedGlyph::PositionedGlyph() noexcept
38 : character (0), glyph (0), x (0), y (0), w (0), whitespace (false)
42 PositionedGlyph::PositionedGlyph (const Font
& font_
, juce_wchar character_
, int glyphNumber
,
43 float anchorX
, float baselineY
, float width
, bool whitespace_
)
44 : font (font_
), character (character_
), glyph (glyphNumber
),
45 x (anchorX
), y (baselineY
), w (width
), whitespace (whitespace_
)
49 static 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
);
56 void PositionedGlyph::draw (Graphics
& g
) const
59 drawGlyphWithFont (g
, glyph
, font
, AffineTransform::translation (x
, y
));
62 void PositionedGlyph::draw (Graphics
& g
, AffineTransform transform
) const
65 drawGlyphWithFont (g
, glyph
, font
, AffineTransform::translation (x
, y
).followedBy (transform
));
68 void 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())
83 bool 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
);
96 return p
.contains (px
, py
);
103 void PositionedGlyph::moveBy (float deltaX
, float deltaY
)
110 //==============================================================================
111 GlyphArrangement::GlyphArrangement()
113 glyphs
.ensureStorageAllocated (128);
116 //==============================================================================
117 void GlyphArrangement::clear()
122 PositionedGlyph
& GlyphArrangement::getGlyph (int index
) noexcept
124 return glyphs
.getReference (index
);
127 //==============================================================================
128 void GlyphArrangement::addGlyphArrangement (const GlyphArrangement
& other
)
130 glyphs
.addArray (other
.glyphs
);
133 void GlyphArrangement::addGlyph (const PositionedGlyph
& glyph
)
138 void GlyphArrangement::removeRangeOfGlyphs (int startIndex
, int num
)
140 glyphs
.removeRange (startIndex
, num
< 0 ? glyphs
.size() : num
);
143 //==============================================================================
144 void GlyphArrangement::addLineOfText (const Font
& font
, const String
& text
, float xOffset
, float yOffset
)
146 addCurtailedLineOfText (font
, text
, xOffset
, yOffset
, 1.0e10f
, false);
149 void GlyphArrangement::addCurtailedLineOfText (const Font
& font
, const String
& text
,
150 float xOffset
, float yOffset
,
151 float maxWidthPixels
, bool useEllipsis
)
153 if (text
.isNotEmpty())
155 Array
<int> newGlyphs
;
156 Array
<float> xOffsets
;
157 font
.getGlyphPositions (text
, newGlyphs
, xOffsets
);
158 auto textLen
= newGlyphs
.size();
159 glyphs
.ensureStorageAllocated (glyphs
.size() + textLen
);
161 auto t
= text
.getCharPointer();
163 for (int i
= 0; i
< textLen
; ++i
)
165 auto nextX
= xOffsets
.getUnchecked (i
+ 1);
167 if (nextX
> maxWidthPixels
+ 1.0f
)
169 // curtail the string if it's too wide..
170 if (useEllipsis
&& textLen
> 3 && glyphs
.size() >= 3)
171 insertEllipsis (font
, xOffset
+ maxWidthPixels
, 0, glyphs
.size());
176 auto thisX
= xOffsets
.getUnchecked (i
);
177 auto isWhitespace
= isNonBreakingSpace (*t
) || t
.isWhitespace();
179 glyphs
.add (PositionedGlyph (font
, t
.getAndAdvance(),
180 newGlyphs
.getUnchecked(i
),
181 xOffset
+ thisX
, yOffset
,
182 nextX
- thisX
, isWhitespace
));
187 int GlyphArrangement::insertEllipsis (const Font
& font
, float maxXPos
, int startIndex
, int endIndex
)
191 if (! glyphs
.isEmpty())
193 Array
<int> dotGlyphs
;
195 font
.getGlyphPositions ("..", dotGlyphs
, dotXs
);
198 float xOffset
= 0.0f
, yOffset
= 0.0f
;
200 while (endIndex
> startIndex
)
202 auto& pg
= glyphs
.getReference (--endIndex
);
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
)
228 void GlyphArrangement::addJustifiedText (const Font
& font
, const String
& text
,
229 float x
, float y
, float maxLineWidth
,
230 Justification horizontalLayout
,
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
);
306 y
+= font
.getHeight() + leading
;
310 void GlyphArrangement::addFittedText (const Font
& f
, const String
& text
,
311 float x
, float y
, float width
, float height
,
312 Justification layout
, int maximumLines
,
313 float minimumHorizontalScale
)
315 if (minimumHorizontalScale
== 0.0f
)
316 minimumHorizontalScale
= Font::getDefaultMinimumHorizontalScaleFactor();
318 // doesn't make much sense if this is outside a sensible range of 0.5 to 1.0
319 jassert (minimumHorizontalScale
> 0 && minimumHorizontalScale
<= 1.0f
);
321 if (text
.containsAnyOf ("\r\n"))
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
);
361 //==============================================================================
362 void GlyphArrangement::moveRangeOfGlyphs (int startIndex
, int num
, const float dx
, const float dy
)
364 jassert (startIndex
>= 0);
366 if (dx
!= 0.0f
|| dy
!= 0.0f
)
368 if (num
< 0 || startIndex
+ num
> glyphs
.size())
369 num
= glyphs
.size() - startIndex
;
372 glyphs
.getReference (startIndex
++).moveBy (dx
, dy
);
376 void GlyphArrangement::addLinesWithLineBreaks (const String
& text
, const Font
& f
,
377 float x
, float y
, float width
, float height
, Justification layout
)
380 ga
.addJustifiedText (f
, text
, x
, y
, width
, layout
);
382 auto bb
= ga
.getBoundingBox (0, -1, false);
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());
388 ga
.moveRangeOfGlyphs (0, -1, 0.0f
, dy
);
390 glyphs
.addArray (ga
.glyphs
);
393 int 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
);
419 void GlyphArrangement::stretchRangeOfGlyphs (int startIndex
, int num
, float horizontalScaleFactor
)
421 jassert (startIndex
>= 0);
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
;
441 Rectangle
<float> GlyphArrangement::getBoundingBox (int startIndex
, int num
, bool includeWhitespace
) const
443 jassert (startIndex
>= 0);
445 if (num
< 0 || startIndex
+ num
> glyphs
.size())
446 num
= glyphs
.size() - startIndex
;
448 Rectangle
<float> result
;
452 auto& pg
= glyphs
.getReference (startIndex
++);
454 if (includeWhitespace
|| ! pg
.isWhitespace())
455 result
= result
.getUnion (pg
.getBounds());
461 void GlyphArrangement::justifyGlyphs (int startIndex
, int num
,
462 float x
, float y
, float width
, float height
,
463 Justification justification
)
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
);
509 void 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
;
552 static bool isBreakableGlyph (const PositionedGlyph
& g
) noexcept
554 return ! isNonBreakingSpace (g
.getCharacter()) && (g
.isWhitespace() || g
.getCharacter() == '-');
557 void 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();
586 // Try to estimate the point at which there are enough lines to fit the text,
587 // allowing for unevenness in the lengths due to differently sized words.
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
)
620 // got to a point where the line's too long, so skip forward to find a
621 // good place to break it..
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
))
638 // can't find a suitable break, so try looking backwards..
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
);
691 //==============================================================================
692 void 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 && 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
);
706 void GlyphArrangement::draw (const Graphics
& g
) const
711 void GlyphArrangement::draw (const Graphics
& g
, AffineTransform transform
) 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();
748 void GlyphArrangement::createPath (Path
& path
) const
750 for (auto& g
: glyphs
)
754 int GlyphArrangement::findGlyphIndexAt (float x
, float y
) const
756 for (int i
= 0; i
< glyphs
.size(); ++i
)
757 if (glyphs
.getReference (i
).hitTest (x
, y
))