1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
PositionedGlyph()29 PositionedGlyph::PositionedGlyph() noexcept
30     : character (0), glyph (0), x (0), y (0), w (0), whitespace (false)
31 {
32 }
33 
PositionedGlyph(const Font & font_,juce_wchar character_,int glyphNumber,float anchorX,float baselineY,float width,bool whitespace_)34 PositionedGlyph::PositionedGlyph (const Font& font_, juce_wchar character_, int glyphNumber,
35                                   float anchorX, float baselineY, float width, bool whitespace_)
36     : font (font_), character (character_), glyph (glyphNumber),
37       x (anchorX), y (baselineY), w (width), whitespace (whitespace_)
38 {
39 }
40 
~PositionedGlyph()41 PositionedGlyph::~PositionedGlyph() {}
42 
drawGlyphWithFont(Graphics & g,int glyph,const Font & font,AffineTransform t)43 static void drawGlyphWithFont (Graphics& g, int glyph, const Font& font, AffineTransform t)
44 {
45     auto& context = g.getInternalContext();
46     context.setFont (font);
47     context.drawGlyph (glyph, t);
48 }
49 
draw(Graphics & g) const50 void PositionedGlyph::draw (Graphics& g) const
51 {
52     if (! isWhitespace())
53         drawGlyphWithFont (g, glyph, font, AffineTransform::translation (x, y));
54 }
55 
draw(Graphics & g,AffineTransform transform) const56 void PositionedGlyph::draw (Graphics& g, AffineTransform transform) const
57 {
58     if (! isWhitespace())
59         drawGlyphWithFont (g, glyph, font, AffineTransform::translation (x, y).followedBy (transform));
60 }
61 
createPath(Path & path) const62 void PositionedGlyph::createPath (Path& path) const
63 {
64     if (! isWhitespace())
65     {
66         if (auto* t = font.getTypeface())
67         {
68             Path p;
69             t->getOutlineForGlyph (glyph, p);
70 
71             path.addPath (p, AffineTransform::scale (font.getHeight() * font.getHorizontalScale(), font.getHeight())
72                                              .translated (x, y));
73         }
74     }
75 }
76 
hitTest(float px,float py) const77 bool PositionedGlyph::hitTest (float px, float py) const
78 {
79     if (getBounds().contains (px, py) && ! isWhitespace())
80     {
81         if (auto* t = font.getTypeface())
82         {
83             Path p;
84             t->getOutlineForGlyph (glyph, p);
85 
86             AffineTransform::translation (-x, -y)
87                             .scaled (1.0f / (font.getHeight() * font.getHorizontalScale()), 1.0f / font.getHeight())
88                             .transformPoint (px, py);
89 
90             return p.contains (px, py);
91         }
92     }
93 
94     return false;
95 }
96 
moveBy(float deltaX,float deltaY)97 void PositionedGlyph::moveBy (float deltaX, float deltaY)
98 {
99     x += deltaX;
100     y += deltaY;
101 }
102 
103 
104 //==============================================================================
GlyphArrangement()105 GlyphArrangement::GlyphArrangement()
106 {
107     glyphs.ensureStorageAllocated (128);
108 }
109 
110 //==============================================================================
clear()111 void GlyphArrangement::clear()
112 {
113     glyphs.clear();
114 }
115 
getGlyph(int index)116 PositionedGlyph& GlyphArrangement::getGlyph (int index) noexcept
117 {
118     return glyphs.getReference (index);
119 }
120 
121 //==============================================================================
addGlyphArrangement(const GlyphArrangement & other)122 void GlyphArrangement::addGlyphArrangement (const GlyphArrangement& other)
123 {
124     glyphs.addArray (other.glyphs);
125 }
126 
addGlyph(const PositionedGlyph & glyph)127 void GlyphArrangement::addGlyph (const PositionedGlyph& glyph)
128 {
129     glyphs.add (glyph);
130 }
131 
removeRangeOfGlyphs(int startIndex,int num)132 void GlyphArrangement::removeRangeOfGlyphs (int startIndex, int num)
133 {
134     glyphs.removeRange (startIndex, num < 0 ? glyphs.size() : num);
135 }
136 
137 //==============================================================================
addLineOfText(const Font & font,const String & text,float xOffset,float yOffset)138 void GlyphArrangement::addLineOfText (const Font& font, const String& text, float xOffset, float yOffset)
139 {
140     addCurtailedLineOfText (font, text, xOffset, yOffset, 1.0e10f, false);
141 }
142 
addCurtailedLineOfText(const Font & font,const String & text,float xOffset,float yOffset,float maxWidthPixels,bool useEllipsis)143 void GlyphArrangement::addCurtailedLineOfText (const Font& font, const String& text,
144                                                float xOffset, float yOffset,
145                                                float maxWidthPixels, bool useEllipsis)
146 {
147     if (text.isNotEmpty())
148     {
149         Array<int> newGlyphs;
150         Array<float> xOffsets;
151         font.getGlyphPositions (text, newGlyphs, xOffsets);
152         auto textLen = newGlyphs.size();
153         glyphs.ensureStorageAllocated (glyphs.size() + textLen);
154 
155         auto t = text.getCharPointer();
156 
157         for (int i = 0; i < textLen; ++i)
158         {
159             auto nextX = xOffsets.getUnchecked (i + 1);
160 
161             if (nextX > maxWidthPixels + 1.0f)
162             {
163                 // curtail the string if it's too wide..
164                 if (useEllipsis && textLen > 3 && glyphs.size() >= 3)
165                     insertEllipsis (font, xOffset + maxWidthPixels, 0, glyphs.size());
166 
167                 break;
168             }
169 
170             auto thisX = xOffsets.getUnchecked (i);
171             bool isWhitespace = t.isWhitespace();
172 
173             glyphs.add (PositionedGlyph (font, t.getAndAdvance(),
174                                          newGlyphs.getUnchecked(i),
175                                          xOffset + thisX, yOffset,
176                                          nextX - thisX, isWhitespace));
177         }
178     }
179 }
180 
insertEllipsis(const Font & font,float maxXPos,int startIndex,int endIndex)181 int GlyphArrangement::insertEllipsis (const Font& font, float maxXPos, int startIndex, int endIndex)
182 {
183     int numDeleted = 0;
184 
185     if (! glyphs.isEmpty())
186     {
187         Array<int> dotGlyphs;
188         Array<float> dotXs;
189         font.getGlyphPositions ("..", dotGlyphs, dotXs);
190 
191         auto dx = dotXs[1];
192         float xOffset = 0.0f, yOffset = 0.0f;
193 
194         while (endIndex > startIndex)
195         {
196             auto& pg = glyphs.getReference (--endIndex);
197             xOffset = pg.x;
198             yOffset = pg.y;
199 
200             glyphs.remove (endIndex);
201             ++numDeleted;
202 
203             if (xOffset + dx * 3 <= maxXPos)
204                 break;
205         }
206 
207         for (int i = 3; --i >= 0;)
208         {
209             glyphs.insert (endIndex++, PositionedGlyph (font, '.', dotGlyphs.getFirst(),
210                                                         xOffset, yOffset, dx, false));
211             --numDeleted;
212             xOffset += dx;
213 
214             if (xOffset > maxXPos)
215                 break;
216         }
217     }
218 
219     return numDeleted;
220 }
221 
addJustifiedText(const Font & font,const String & text,float x,float y,float maxLineWidth,Justification horizontalLayout,float leading)222 void GlyphArrangement::addJustifiedText (const Font& font, const String& text,
223                                          float x, float y, float maxLineWidth,
224                                          Justification horizontalLayout,
225                                          float leading)
226 {
227     auto lineStartIndex = glyphs.size();
228     addLineOfText (font, text, x, y);
229 
230     auto originalY = y;
231 
232     while (lineStartIndex < glyphs.size())
233     {
234         int i = lineStartIndex;
235 
236         if (glyphs.getReference(i).getCharacter() != '\n'
237               && glyphs.getReference(i).getCharacter() != '\r')
238             ++i;
239 
240         auto lineMaxX = glyphs.getReference (lineStartIndex).getLeft() + maxLineWidth;
241         int lastWordBreakIndex = -1;
242 
243         while (i < glyphs.size())
244         {
245             auto& pg = glyphs.getReference (i);
246             auto c = pg.getCharacter();
247 
248             if (c == '\r' || c == '\n')
249             {
250                 ++i;
251 
252                 if (c == '\r' && i < glyphs.size()
253                      && glyphs.getReference(i).getCharacter() == '\n')
254                     ++i;
255 
256                 break;
257             }
258 
259             if (pg.isWhitespace())
260             {
261                 lastWordBreakIndex = i + 1;
262             }
263             else if (pg.getRight() - 0.0001f >= lineMaxX)
264             {
265                 if (lastWordBreakIndex >= 0)
266                     i = lastWordBreakIndex;
267 
268                 break;
269             }
270 
271             ++i;
272         }
273 
274         auto currentLineStartX = glyphs.getReference (lineStartIndex).getLeft();
275         auto currentLineEndX = currentLineStartX;
276 
277         for (int j = i; --j >= lineStartIndex;)
278         {
279             if (! glyphs.getReference (j).isWhitespace())
280             {
281                 currentLineEndX = glyphs.getReference (j).getRight();
282                 break;
283             }
284         }
285 
286         float deltaX = 0.0f;
287 
288         if (horizontalLayout.testFlags (Justification::horizontallyJustified))
289             spreadOutLine (lineStartIndex, i - lineStartIndex, maxLineWidth);
290         else if (horizontalLayout.testFlags (Justification::horizontallyCentred))
291             deltaX = (maxLineWidth - (currentLineEndX - currentLineStartX)) * 0.5f;
292         else if (horizontalLayout.testFlags (Justification::right))
293             deltaX = maxLineWidth - (currentLineEndX - currentLineStartX);
294 
295         moveRangeOfGlyphs (lineStartIndex, i - lineStartIndex,
296                            x + deltaX - currentLineStartX, y - originalY);
297 
298         lineStartIndex = i;
299 
300         y += font.getHeight() + leading;
301     }
302 }
303 
addFittedText(const Font & f,const String & text,float x,float y,float width,float height,Justification layout,int maximumLines,float minimumHorizontalScale)304 void GlyphArrangement::addFittedText (const Font& f, const String& text,
305                                       float x, float y, float width, float height,
306                                       Justification layout, int maximumLines,
307                                       float minimumHorizontalScale)
308 {
309     if (minimumHorizontalScale == 0.0f)
310         minimumHorizontalScale = Font::getDefaultMinimumHorizontalScaleFactor();
311 
312     // doesn't make much sense if this is outside a sensible range of 0.5 to 1.0
313     jassert (minimumHorizontalScale > 0 && minimumHorizontalScale <= 1.0f);
314 
315     if (text.containsAnyOf ("\r\n"))
316     {
317         addLinesWithLineBreaks (text, f, x, y, width, height, layout);
318     }
319     else
320     {
321         auto startIndex = glyphs.size();
322         auto trimmed = text.trim();
323         addLineOfText (f, trimmed, x, y);
324         auto numGlyphs = glyphs.size() - startIndex;
325 
326         if (numGlyphs > 0)
327         {
328             auto lineWidth = glyphs.getReference (glyphs.size() - 1).getRight()
329                                 - glyphs.getReference (startIndex).getLeft();
330 
331             if (lineWidth > 0)
332             {
333                 if (lineWidth * minimumHorizontalScale < width)
334                 {
335                     if (lineWidth > width)
336                         stretchRangeOfGlyphs (startIndex, numGlyphs, width / lineWidth);
337 
338                     justifyGlyphs (startIndex, numGlyphs, x, y, width, height, layout);
339                 }
340                 else if (maximumLines <= 1)
341                 {
342                     fitLineIntoSpace (startIndex, numGlyphs, x, y, width, height,
343                                       f, layout, minimumHorizontalScale);
344                 }
345                 else
346                 {
347                     splitLines (trimmed, f, startIndex, x, y, width, height,
348                                 maximumLines, lineWidth, layout, minimumHorizontalScale);
349                 }
350             }
351         }
352     }
353 }
354 
355 //==============================================================================
moveRangeOfGlyphs(int startIndex,int num,const float dx,const float dy)356 void GlyphArrangement::moveRangeOfGlyphs (int startIndex, int num, const float dx, const float dy)
357 {
358     jassert (startIndex >= 0);
359 
360     if (dx != 0.0f || dy != 0.0f)
361     {
362         if (num < 0 || startIndex + num > glyphs.size())
363             num = glyphs.size() - startIndex;
364 
365         while (--num >= 0)
366             glyphs.getReference (startIndex++).moveBy (dx, dy);
367     }
368 }
369 
addLinesWithLineBreaks(const String & text,const Font & f,float x,float y,float width,float height,Justification layout)370 void GlyphArrangement::addLinesWithLineBreaks (const String& text, const Font& f,
371                                                float x, float y, float width, float height, Justification layout)
372 {
373     GlyphArrangement ga;
374     ga.addJustifiedText (f, text, x, y, width, layout);
375 
376     auto bb = ga.getBoundingBox (0, -1, false);
377     auto dy = y - bb.getY();
378 
379     if (layout.testFlags (Justification::verticallyCentred))   dy += (height - bb.getHeight()) * 0.5f;
380     else if (layout.testFlags (Justification::bottom))         dy += (height - bb.getHeight());
381 
382     ga.moveRangeOfGlyphs (0, -1, 0.0f, dy);
383 
384     glyphs.addArray (ga.glyphs);
385 }
386 
fitLineIntoSpace(int start,int numGlyphs,float x,float y,float w,float h,const Font & font,Justification justification,float minimumHorizontalScale)387 int GlyphArrangement::fitLineIntoSpace (int start, int numGlyphs, float x, float y, float w, float h, const Font& font,
388                                         Justification justification, float minimumHorizontalScale)
389 {
390     int numDeleted = 0;
391     auto lineStartX = glyphs.getReference (start).getLeft();
392     auto lineWidth  = glyphs.getReference (start + numGlyphs - 1).getRight() - lineStartX;
393 
394     if (lineWidth > w)
395     {
396         if (minimumHorizontalScale < 1.0f)
397         {
398             stretchRangeOfGlyphs (start, numGlyphs, jmax (minimumHorizontalScale, w / lineWidth));
399             lineWidth = glyphs.getReference (start + numGlyphs - 1).getRight() - lineStartX - 0.5f;
400         }
401 
402         if (lineWidth > w)
403         {
404             numDeleted = insertEllipsis (font, lineStartX + w, start, start + numGlyphs);
405             numGlyphs -= numDeleted;
406         }
407     }
408 
409     justifyGlyphs (start, numGlyphs, x, y, w, h, justification);
410     return numDeleted;
411 }
412 
stretchRangeOfGlyphs(int startIndex,int num,float horizontalScaleFactor)413 void GlyphArrangement::stretchRangeOfGlyphs (int startIndex, int num, float horizontalScaleFactor)
414 {
415     jassert (startIndex >= 0);
416 
417     if (num < 0 || startIndex + num > glyphs.size())
418         num = glyphs.size() - startIndex;
419 
420     if (num > 0)
421     {
422         auto xAnchor = glyphs.getReference (startIndex).getLeft();
423 
424         while (--num >= 0)
425         {
426             auto& pg = glyphs.getReference (startIndex++);
427 
428             pg.x = xAnchor + (pg.x - xAnchor) * horizontalScaleFactor;
429             pg.font.setHorizontalScale (pg.font.getHorizontalScale() * horizontalScaleFactor);
430             pg.w *= horizontalScaleFactor;
431         }
432     }
433 }
434 
getBoundingBox(int startIndex,int num,bool includeWhitespace) const435 Rectangle<float> GlyphArrangement::getBoundingBox (int startIndex, int num, bool includeWhitespace) const
436 {
437     jassert (startIndex >= 0);
438 
439     if (num < 0 || startIndex + num > glyphs.size())
440         num = glyphs.size() - startIndex;
441 
442     Rectangle<float> result;
443 
444     while (--num >= 0)
445     {
446         auto& pg = glyphs.getReference (startIndex++);
447 
448         if (includeWhitespace || ! pg.isWhitespace())
449             result = result.getUnion (pg.getBounds());
450     }
451 
452     return result;
453 }
454 
justifyGlyphs(int startIndex,int num,float x,float y,float width,float height,Justification justification)455 void GlyphArrangement::justifyGlyphs (int startIndex, int num,
456                                       float x, float y, float width, float height,
457                                       Justification justification)
458 {
459     jassert (num >= 0 && startIndex >= 0);
460 
461     if (glyphs.size() > 0 && num > 0)
462     {
463         auto bb = getBoundingBox (startIndex, num, ! justification.testFlags (Justification::horizontallyJustified
464                                                                                | Justification::horizontallyCentred));
465         float deltaX = x, deltaY = y;
466 
467         if (justification.testFlags (Justification::horizontallyJustified))     deltaX -= bb.getX();
468         else if (justification.testFlags (Justification::horizontallyCentred))  deltaX += (width - bb.getWidth()) * 0.5f - bb.getX();
469         else if (justification.testFlags (Justification::right))                deltaX += width - bb.getRight();
470         else                                                                    deltaX -= bb.getX();
471 
472         if (justification.testFlags (Justification::top))                       deltaY -= bb.getY();
473         else if (justification.testFlags (Justification::bottom))               deltaY += height - bb.getBottom();
474         else                                                                    deltaY += (height - bb.getHeight()) * 0.5f - bb.getY();
475 
476         moveRangeOfGlyphs (startIndex, num, deltaX, deltaY);
477 
478         if (justification.testFlags (Justification::horizontallyJustified))
479         {
480             int lineStart = 0;
481             auto baseY = glyphs.getReference (startIndex).getBaselineY();
482 
483             int i;
484             for (i = 0; i < num; ++i)
485             {
486                 auto glyphY = glyphs.getReference (startIndex + i).getBaselineY();
487 
488                 if (glyphY != baseY)
489                 {
490                     spreadOutLine (startIndex + lineStart, i - lineStart, width);
491 
492                     lineStart = i;
493                     baseY = glyphY;
494                 }
495             }
496 
497             if (i > lineStart)
498                 spreadOutLine (startIndex + lineStart, i - lineStart, width);
499         }
500     }
501 }
502 
spreadOutLine(int start,int num,float targetWidth)503 void GlyphArrangement::spreadOutLine (int start, int num, float targetWidth)
504 {
505     if (start + num < glyphs.size()
506          && glyphs.getReference (start + num - 1).getCharacter() != '\r'
507          && glyphs.getReference (start + num - 1).getCharacter() != '\n')
508     {
509         int numSpaces = 0;
510         int spacesAtEnd = 0;
511 
512         for (int i = 0; i < num; ++i)
513         {
514             if (glyphs.getReference (start + i).isWhitespace())
515             {
516                 ++spacesAtEnd;
517                 ++numSpaces;
518             }
519             else
520             {
521                 spacesAtEnd = 0;
522             }
523         }
524 
525         numSpaces -= spacesAtEnd;
526 
527         if (numSpaces > 0)
528         {
529             auto startX = glyphs.getReference (start).getLeft();
530             auto endX   = glyphs.getReference (start + num - 1 - spacesAtEnd).getRight();
531 
532             auto extraPaddingBetweenWords = (targetWidth - (endX - startX)) / (float) numSpaces;
533             float deltaX = 0.0f;
534 
535             for (int i = 0; i < num; ++i)
536             {
537                 glyphs.getReference (start + i).moveBy (deltaX, 0.0f);
538 
539                 if (glyphs.getReference (start + i).isWhitespace())
540                     deltaX += extraPaddingBetweenWords;
541             }
542         }
543     }
544 }
545 
isBreakableGlyph(const PositionedGlyph & g)546 static bool isBreakableGlyph (const PositionedGlyph& g) noexcept
547 {
548     return g.isWhitespace() || g.getCharacter() == '-';
549 }
550 
splitLines(const String & text,Font font,int startIndex,float x,float y,float width,float height,int maximumLines,float lineWidth,Justification layout,float minimumHorizontalScale)551 void GlyphArrangement::splitLines (const String& text, Font font, int startIndex,
552                                    float x, float y, float width, float height, int maximumLines,
553                                    float lineWidth, Justification layout, float minimumHorizontalScale)
554 {
555     auto length = text.length();
556     auto originalStartIndex = startIndex;
557     int numLines = 1;
558 
559     if (length <= 12 && ! text.containsAnyOf (" -\t\r\n"))
560         maximumLines = 1;
561 
562     maximumLines = jmin (maximumLines, length);
563 
564     while (numLines < maximumLines)
565     {
566         ++numLines;
567         auto newFontHeight = height / (float) numLines;
568 
569         if (newFontHeight < font.getHeight())
570         {
571             font.setHeight (jmax (8.0f, newFontHeight));
572 
573             removeRangeOfGlyphs (startIndex, -1);
574             addLineOfText (font, text, x, y);
575 
576             lineWidth = glyphs.getReference (glyphs.size() - 1).getRight()
577                             - glyphs.getReference (startIndex).getLeft();
578         }
579 
580         // Try to estimate the point at which there are enough lines to fit the text,
581         // allowing for unevenness in the lengths due to differently sized words.
582         const float lineLengthUnevennessAllowance = 80.0f;
583 
584         if ((float) numLines > (lineWidth + lineLengthUnevennessAllowance) / width || newFontHeight < 8.0f)
585             break;
586     }
587 
588     if (numLines < 1)
589         numLines = 1;
590 
591     int lineIndex = 0;
592     auto lineY = y;
593     auto widthPerLine = jmin (width / minimumHorizontalScale,
594                               lineWidth / (float) numLines);
595 
596     while (lineY < y + height)
597     {
598         auto endIndex = startIndex;
599         auto lineStartX = glyphs.getReference (startIndex).getLeft();
600         auto lineBottomY = lineY + font.getHeight();
601 
602         if (lineIndex++ >= numLines - 1
603              || lineBottomY >= y + height)
604         {
605             widthPerLine = width;
606             endIndex = glyphs.size();
607         }
608         else
609         {
610             while (endIndex < glyphs.size())
611             {
612                 if (glyphs.getReference (endIndex).getRight() - lineStartX > widthPerLine)
613                 {
614                     // got to a point where the line's too long, so skip forward to find a
615                     // good place to break it..
616                     auto searchStartIndex = endIndex;
617 
618                     while (endIndex < glyphs.size())
619                     {
620                         auto& g = glyphs.getReference (endIndex);
621 
622                         if ((g.getRight() - lineStartX) * minimumHorizontalScale < width)
623                         {
624                             if (isBreakableGlyph (g))
625                             {
626                                 ++endIndex;
627                                 break;
628                             }
629                         }
630                         else
631                         {
632                             // can't find a suitable break, so try looking backwards..
633                             endIndex = searchStartIndex;
634 
635                             for (int back = 1; back < jmin (7, endIndex - startIndex - 1); ++back)
636                             {
637                                 if (isBreakableGlyph (glyphs.getReference (endIndex - back)))
638                                 {
639                                     endIndex -= back - 1;
640                                     break;
641                                 }
642                             }
643 
644                             break;
645                         }
646 
647                         ++endIndex;
648                     }
649 
650                     break;
651                 }
652 
653                 ++endIndex;
654             }
655 
656             auto wsStart = endIndex;
657             auto wsEnd   = endIndex;
658 
659             while (wsStart > 0 && glyphs.getReference (wsStart - 1).isWhitespace())
660                 --wsStart;
661 
662             while (wsEnd < glyphs.size() && glyphs.getReference (wsEnd).isWhitespace())
663                 ++wsEnd;
664 
665             removeRangeOfGlyphs (wsStart, wsEnd - wsStart);
666             endIndex = jmax (wsStart, startIndex + 1);
667         }
668 
669         endIndex -= fitLineIntoSpace (startIndex, endIndex - startIndex,
670                                       x, lineY, width, font.getHeight(), font,
671                                       layout.getOnlyHorizontalFlags() | Justification::verticallyCentred,
672                                       minimumHorizontalScale);
673 
674         startIndex = endIndex;
675         lineY = lineBottomY;
676 
677         if (startIndex >= glyphs.size())
678             break;
679     }
680 
681     justifyGlyphs (originalStartIndex, glyphs.size() - originalStartIndex,
682                    x, y, width, height, layout.getFlags() & ~Justification::horizontallyJustified);
683 }
684 
685 //==============================================================================
drawGlyphUnderline(const Graphics & g,const PositionedGlyph & pg,int i,AffineTransform transform) const686 void GlyphArrangement::drawGlyphUnderline (const Graphics& g, const PositionedGlyph& pg,
687                                            int i, AffineTransform transform) const
688 {
689     auto lineThickness = (pg.font.getDescent()) * 0.3f;
690     auto nextX = pg.x + pg.w;
691 
692     if (i < glyphs.size() - 1 && glyphs.getReference (i + 1).y == pg.y)
693         nextX = glyphs.getReference (i + 1).x;
694 
695     Path p;
696     p.addRectangle (pg.x, pg.y + lineThickness * 2.0f, nextX - pg.x, lineThickness);
697     g.fillPath (p, transform);
698 }
699 
draw(const Graphics & g) const700 void GlyphArrangement::draw (const Graphics& g) const
701 {
702     draw (g, {});
703 }
704 
draw(const Graphics & g,AffineTransform transform) const705 void GlyphArrangement::draw (const Graphics& g, AffineTransform transform) const
706 {
707     auto& context = g.getInternalContext();
708     auto lastFont = context.getFont();
709     bool needToRestore = false;
710 
711     for (int i = 0; i < glyphs.size(); ++i)
712     {
713         auto& pg = glyphs.getReference (i);
714 
715         if (pg.font.isUnderlined())
716             drawGlyphUnderline (g, pg, i, transform);
717 
718         if (! pg.isWhitespace())
719         {
720             if (lastFont != pg.font)
721             {
722                 lastFont = pg.font;
723 
724                 if (! needToRestore)
725                 {
726                     needToRestore = true;
727                     context.saveState();
728                 }
729 
730                 context.setFont (lastFont);
731             }
732 
733             context.drawGlyph (pg.glyph, AffineTransform::translation (pg.x, pg.y)
734                                                          .followedBy (transform));
735         }
736     }
737 
738     if (needToRestore)
739         context.restoreState();
740 }
741 
createPath(Path & path) const742 void GlyphArrangement::createPath (Path& path) const
743 {
744     for (auto& g : glyphs)
745         g.createPath (path);
746 }
747 
findGlyphIndexAt(float x,float y) const748 int GlyphArrangement::findGlyphIndexAt (float x, float y) const
749 {
750     for (int i = 0; i < glyphs.size(); ++i)
751         if (glyphs.getReference (i).hitTest (x, y))
752             return i;
753 
754     return -1;
755 }
756 
757 } // namespace juce
758