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 
substring(const String & text,Range<int> range)29 static String substring (const String& text, Range<int> range)
30 {
31     return text.substring (range.getStart(), range.getEnd());
32 }
33 
Glyph(int glyph,Point<float> anch,float w)34 TextLayout::Glyph::Glyph (int glyph, Point<float> anch, float w) noexcept
35     : glyphCode (glyph), anchor (anch), width (w)
36 {
37 }
38 
Glyph(const Glyph & other)39 TextLayout::Glyph::Glyph (const Glyph& other) noexcept
40     : glyphCode (other.glyphCode), anchor (other.anchor), width (other.width)
41 {
42 }
43 
operator =(const Glyph & other)44 TextLayout::Glyph& TextLayout::Glyph::operator= (const Glyph& other) noexcept
45 {
46     glyphCode = other.glyphCode;
47     anchor = other.anchor;
48     width = other.width;
49     return *this;
50 }
51 
~Glyph()52 TextLayout::Glyph::~Glyph() noexcept {}
53 
54 //==============================================================================
Run()55 TextLayout::Run::Run() noexcept
56     : colour (0xff000000)
57 {
58 }
59 
Run(Range<int> range,int numGlyphsToPreallocate)60 TextLayout::Run::Run (Range<int> range, int numGlyphsToPreallocate)
61     : colour (0xff000000), stringRange (range)
62 {
63     glyphs.ensureStorageAllocated (numGlyphsToPreallocate);
64 }
65 
Run(const Run & other)66 TextLayout::Run::Run (const Run& other)
67     : font (other.font),
68       colour (other.colour),
69       glyphs (other.glyphs),
70       stringRange (other.stringRange)
71 {
72 }
73 
~Run()74 TextLayout::Run::~Run() noexcept {}
75 
getRunBoundsX() const76 Range<float> TextLayout::Run::getRunBoundsX() const noexcept
77 {
78     Range<float> range;
79     bool isFirst = true;
80 
81     for (auto& glyph : glyphs)
82     {
83         Range<float> r (glyph.anchor.x, glyph.anchor.x + glyph.width);
84 
85         if (isFirst)
86         {
87             isFirst = false;
88             range = r;
89         }
90         else
91         {
92             range = range.getUnionWith (r);
93         }
94     }
95 
96     return range;
97 }
98 
99 //==============================================================================
Line()100 TextLayout::Line::Line() noexcept
101     : ascent (0.0f), descent (0.0f), leading (0.0f)
102 {
103 }
104 
Line(Range<int> range,Point<float> o,float asc,float desc,float lead,int numRunsToPreallocate)105 TextLayout::Line::Line (Range<int> range, Point<float> o, float asc, float desc,
106                         float lead, int numRunsToPreallocate)
107     : stringRange (range), lineOrigin (o),
108       ascent (asc), descent (desc), leading (lead)
109 {
110     runs.ensureStorageAllocated (numRunsToPreallocate);
111 }
112 
Line(const Line & other)113 TextLayout::Line::Line (const Line& other)
114     : stringRange (other.stringRange), lineOrigin (other.lineOrigin),
115       ascent (other.ascent), descent (other.descent), leading (other.leading)
116 {
117     runs.addCopiesOf (other.runs);
118 }
119 
~Line()120 TextLayout::Line::~Line() noexcept
121 {
122 }
123 
getLineBoundsX() const124 Range<float> TextLayout::Line::getLineBoundsX() const noexcept
125 {
126     Range<float> range;
127     bool isFirst = true;
128 
129     for (auto* run : runs)
130     {
131         auto runRange = run->getRunBoundsX();
132 
133         if (isFirst)
134         {
135             isFirst = false;
136             range = runRange;
137         }
138         else
139         {
140             range = range.getUnionWith (runRange);
141         }
142     }
143 
144     return range + lineOrigin.x;
145 }
146 
getLineBoundsY() const147 Range<float> TextLayout::Line::getLineBoundsY() const noexcept
148 {
149     return { lineOrigin.y - ascent,
150              lineOrigin.y + descent };
151 }
152 
getLineBounds() const153 Rectangle<float> TextLayout::Line::getLineBounds() const noexcept
154 {
155     auto x = getLineBoundsX();
156     auto y = getLineBoundsY();
157 
158     return { x.getStart(), y.getStart(), x.getLength(), y.getLength() };
159 }
160 
161 //==============================================================================
TextLayout()162 TextLayout::TextLayout()
163     : width (0), height (0), justification (Justification::topLeft)
164 {
165 }
166 
TextLayout(const TextLayout & other)167 TextLayout::TextLayout (const TextLayout& other)
168     : width (other.width), height (other.height),
169       justification (other.justification)
170 {
171     lines.addCopiesOf (other.lines);
172 }
173 
TextLayout(TextLayout && other)174 TextLayout::TextLayout (TextLayout&& other) noexcept
175     : lines (std::move (other.lines)),
176       width (other.width), height (other.height),
177       justification (other.justification)
178 {
179 }
180 
operator =(TextLayout && other)181 TextLayout& TextLayout::operator= (TextLayout&& other) noexcept
182 {
183     lines = std::move (other.lines);
184     width = other.width;
185     height = other.height;
186     justification = other.justification;
187     return *this;
188 }
189 
operator =(const TextLayout & other)190 TextLayout& TextLayout::operator= (const TextLayout& other)
191 {
192     width = other.width;
193     height = other.height;
194     justification = other.justification;
195     lines.clear();
196     lines.addCopiesOf (other.lines);
197     return *this;
198 }
199 
~TextLayout()200 TextLayout::~TextLayout()
201 {
202 }
203 
getLine(int index) const204 TextLayout::Line& TextLayout::getLine (int index) const noexcept
205 {
206     return *lines.getUnchecked (index);
207 }
208 
ensureStorageAllocated(int numLinesNeeded)209 void TextLayout::ensureStorageAllocated (int numLinesNeeded)
210 {
211     lines.ensureStorageAllocated (numLinesNeeded);
212 }
213 
addLine(std::unique_ptr<Line> line)214 void TextLayout::addLine (std::unique_ptr<Line> line)
215 {
216     lines.add (line.release());
217 }
218 
draw(Graphics & g,Rectangle<float> area) const219 void TextLayout::draw (Graphics& g, Rectangle<float> area) const
220 {
221     auto origin = justification.appliedToRectangle (Rectangle<float> (width, getHeight()), area).getPosition();
222 
223     auto& context   = g.getInternalContext();
224     context.saveState();
225 
226     auto clip       = context.getClipBounds();
227     auto clipTop    = (float) clip.getY()      - origin.y;
228     auto clipBottom = (float) clip.getBottom() - origin.y;
229 
230     for (auto& line : *this)
231     {
232         auto lineRangeY = line.getLineBoundsY();
233 
234         if (lineRangeY.getEnd() < clipTop)
235             continue;
236 
237         if (lineRangeY.getStart() > clipBottom)
238             break;
239 
240         auto lineOrigin = origin + line.lineOrigin;
241 
242         for (auto* run : line.runs)
243         {
244             context.setFont (run->font);
245             context.setFill (run->colour);
246 
247             for (auto& glyph : run->glyphs)
248                 context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x,
249                                                                                   lineOrigin.y + glyph.anchor.y));
250 
251             if (run->font.isUnderlined())
252             {
253                 auto runExtent = run->getRunBoundsX();
254                 auto lineThickness = run->font.getDescent() * 0.3f;
255 
256                 context.fillRect ({ runExtent.getStart() + lineOrigin.x, lineOrigin.y + lineThickness * 2.0f,
257                                     runExtent.getLength(), lineThickness });
258             }
259         }
260     }
261 
262     context.restoreState();
263 }
264 
createLayout(const AttributedString & text,float maxWidth)265 void TextLayout::createLayout (const AttributedString& text, float maxWidth)
266 {
267     createLayout (text, maxWidth, 1.0e7f);
268 }
269 
createLayout(const AttributedString & text,float maxWidth,float maxHeight)270 void TextLayout::createLayout (const AttributedString& text, float maxWidth, float maxHeight)
271 {
272     lines.clear();
273     width = maxWidth;
274     height = maxHeight;
275     justification = text.getJustification();
276 
277     if (! createNativeLayout (text))
278         createStandardLayout (text);
279 
280     recalculateSize();
281 }
282 
createLayoutWithBalancedLineLengths(const AttributedString & text,float maxWidth)283 void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth)
284 {
285     createLayoutWithBalancedLineLengths (text, maxWidth, 1.0e7f);
286 }
287 
createLayoutWithBalancedLineLengths(const AttributedString & text,float maxWidth,float maxHeight)288 void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth, float maxHeight)
289 {
290     auto minimumWidth = maxWidth / 2.0f;
291     auto bestWidth = maxWidth;
292     float bestLineProportion = 0.0f;
293 
294     while (maxWidth > minimumWidth)
295     {
296         createLayout (text, maxWidth, maxHeight);
297 
298         if (getNumLines() < 2)
299             return;
300 
301         auto line1 = lines.getUnchecked (lines.size() - 1)->getLineBoundsX().getLength();
302         auto line2 = lines.getUnchecked (lines.size() - 2)->getLineBoundsX().getLength();
303         auto shortest = jmin (line1, line2);
304         auto longest  = jmax (line1, line2);
305         auto prop = shortest > 0 ? longest / shortest : 1.0f;
306 
307         if (prop > 0.9f && prop < 1.1f)
308             return;
309 
310         if (prop > bestLineProportion)
311         {
312             bestLineProportion = prop;
313             bestWidth = maxWidth;
314         }
315 
316         maxWidth -= 10.0f;
317     }
318 
319     if (bestWidth != maxWidth)
320         createLayout (text, bestWidth, maxHeight);
321 }
322 
323 //==============================================================================
324 namespace TextLayoutHelpers
325 {
326     struct Token
327     {
Tokenjuce::TextLayoutHelpers::Token328         Token (const String& t, const Font& f, Colour c, bool whitespace)
329             : text (t), font (f), colour (c),
330               area (font.getStringWidthFloat (t), f.getHeight()),
331               isWhitespace (whitespace),
332               isNewLine (t.containsChar ('\n') || t.containsChar ('\r'))
333         {}
334 
335         const String text;
336         const Font font;
337         const Colour colour;
338         Rectangle<float> area;
339         int line;
340         float lineHeight;
341         const bool isWhitespace, isNewLine;
342 
343         Token& operator= (const Token&) = delete;
344     };
345 
346     struct TokenList
347     {
TokenListjuce::TextLayoutHelpers::TokenList348         TokenList() noexcept {}
349 
createLayoutjuce::TextLayoutHelpers::TokenList350         void createLayout (const AttributedString& text, TextLayout& layout)
351         {
352             layout.ensureStorageAllocated (totalLines);
353 
354             addTextRuns (text);
355             layoutRuns (layout.getWidth(), text.getLineSpacing(), text.getWordWrap());
356 
357             int charPosition = 0;
358             int lineStartPosition = 0;
359             int runStartPosition = 0;
360 
361             std::unique_ptr<TextLayout::Line> currentLine;
362             std::unique_ptr<TextLayout::Run> currentRun;
363 
364             bool needToSetLineOrigin = true;
365 
366             for (int i = 0; i < tokens.size(); ++i)
367             {
368                 auto& t = *tokens.getUnchecked (i);
369 
370                 Array<int> newGlyphs;
371                 Array<float> xOffsets;
372                 t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets);
373 
374                 if (currentRun == nullptr)  currentRun  = std::make_unique<TextLayout::Run>();
375                 if (currentLine == nullptr) currentLine = std::make_unique<TextLayout::Line>();
376 
377                 if (newGlyphs.size() > 0)
378                 {
379                     currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size());
380                     auto tokenOrigin = t.area.getPosition().translated (0, t.font.getAscent());
381 
382                     if (needToSetLineOrigin)
383                     {
384                         needToSetLineOrigin = false;
385                         currentLine->lineOrigin = tokenOrigin;
386                     }
387 
388                     auto glyphOffset = tokenOrigin - currentLine->lineOrigin;
389 
390                     for (int j = 0; j < newGlyphs.size(); ++j)
391                     {
392                         auto x = xOffsets.getUnchecked (j);
393                         currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked(j),
394                                                                    glyphOffset.translated (x, 0),
395                                                                    xOffsets.getUnchecked (j + 1) - x));
396                     }
397 
398                     charPosition += newGlyphs.size();
399                 }
400                 else if (t.isWhitespace || t.isNewLine)
401                 {
402                     ++charPosition;
403                 }
404 
405                 if (auto* nextToken = tokens[i + 1])
406                 {
407                     if (t.font != nextToken->font || t.colour != nextToken->colour)
408                     {
409                         addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
410                         runStartPosition = charPosition;
411                     }
412 
413                     if (t.line != nextToken->line)
414                     {
415                         if (currentRun == nullptr)
416                             currentRun = std::make_unique<TextLayout::Run>();
417 
418                         addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
419                         currentLine->stringRange = { lineStartPosition, charPosition };
420 
421                         if (! needToSetLineOrigin)
422                             layout.addLine (std::move (currentLine));
423 
424                         runStartPosition = charPosition;
425                         lineStartPosition = charPosition;
426                         needToSetLineOrigin = true;
427                     }
428                 }
429                 else
430                 {
431                     addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
432                     currentLine->stringRange = { lineStartPosition, charPosition };
433 
434                     if (! needToSetLineOrigin)
435                         layout.addLine (std::move (currentLine));
436 
437                     needToSetLineOrigin = true;
438                 }
439             }
440 
441             if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0)
442             {
443                 auto totalW = layout.getWidth();
444                 bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0;
445 
446                 for (auto& line : layout)
447                 {
448                     auto dx = totalW - line.getLineBoundsX().getLength();
449 
450                     if (isCentred)
451                         dx /= 2.0f;
452 
453                     line.lineOrigin.x += dx;
454                 }
455             }
456         }
457 
458     private:
addRunjuce::TextLayoutHelpers::TokenList459         static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun,
460                             const Token& t, int start, int end)
461         {
462             glyphRun->stringRange = { start, end };
463             glyphRun->font = t.font;
464             glyphRun->colour = t.colour;
465             glyphLine.ascent  = jmax (glyphLine.ascent,  t.font.getAscent());
466             glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent());
467             glyphLine.runs.add (glyphRun);
468         }
469 
getCharacterTypejuce::TextLayoutHelpers::TokenList470         static int getCharacterType (juce_wchar c) noexcept
471         {
472             if (c == '\r' || c == '\n')
473                 return 0;
474 
475             return CharacterFunctions::isWhitespace (c) ? 2 : 1;
476         }
477 
appendTextjuce::TextLayoutHelpers::TokenList478         void appendText (const String& stringText, const Font& font, Colour colour)
479         {
480             auto t = stringText.getCharPointer();
481             String currentString;
482             int lastCharType = 0;
483 
484             for (;;)
485             {
486                 auto c = t.getAndAdvance();
487 
488                 if (c == 0)
489                     break;
490 
491                 auto charType = getCharacterType (c);
492 
493                 if (charType == 0 || charType != lastCharType)
494                 {
495                     if (currentString.isNotEmpty())
496                         tokens.add (new Token (currentString, font, colour,
497                                                lastCharType == 2 || lastCharType == 0));
498 
499                     currentString = String::charToString (c);
500 
501                     if (c == '\r' && *t == '\n')
502                         currentString += t.getAndAdvance();
503                 }
504                 else
505                 {
506                     currentString += c;
507                 }
508 
509                 lastCharType = charType;
510             }
511 
512             if (currentString.isNotEmpty())
513                 tokens.add (new Token (currentString, font, colour, lastCharType == 2));
514         }
515 
layoutRunsjuce::TextLayoutHelpers::TokenList516         void layoutRuns (float maxWidth, float extraLineSpacing, AttributedString::WordWrap wordWrap)
517         {
518             float x = 0, y = 0, h = 0;
519             int i;
520 
521             for (i = 0; i < tokens.size(); ++i)
522             {
523                 auto& t = *tokens.getUnchecked(i);
524                 t.area.setPosition (x, y);
525                 t.line = totalLines;
526                 x += t.area.getWidth();
527                 h = jmax (h, t.area.getHeight() + extraLineSpacing);
528 
529                 auto* nextTok = tokens[i + 1];
530 
531                 if (nextTok == nullptr)
532                     break;
533 
534                 bool tokenTooLarge = (x + nextTok->area.getWidth() > maxWidth);
535 
536                 if (t.isNewLine || ((! nextTok->isWhitespace) && (tokenTooLarge && wordWrap != AttributedString::none)))
537                 {
538                     setLastLineHeight (i + 1, h);
539                     x = 0;
540                     y += h;
541                     h = 0;
542                     ++totalLines;
543                 }
544             }
545 
546             setLastLineHeight (jmin (i + 1, tokens.size()), h);
547             ++totalLines;
548         }
549 
setLastLineHeightjuce::TextLayoutHelpers::TokenList550         void setLastLineHeight (int i, float height) noexcept
551         {
552             while (--i >= 0)
553             {
554                 auto& tok = *tokens.getUnchecked (i);
555 
556                 if (tok.line == totalLines)
557                     tok.lineHeight = height;
558                 else
559                     break;
560             }
561         }
562 
addTextRunsjuce::TextLayoutHelpers::TokenList563         void addTextRuns (const AttributedString& text)
564         {
565             auto numAttributes = text.getNumAttributes();
566             tokens.ensureStorageAllocated (jmax (64, numAttributes));
567 
568             for (int i = 0; i < numAttributes; ++i)
569             {
570                 auto& attr = text.getAttribute (i);
571 
572                 appendText (substring (text.getText(), attr.range),
573                             attr.font, attr.colour);
574             }
575         }
576 
getTrimmedEndIfNotAllWhitespacejuce::TextLayoutHelpers::TokenList577         static String getTrimmedEndIfNotAllWhitespace (const String& s)
578         {
579             auto trimmed = s.trimEnd();
580 
581             if (trimmed.isEmpty() && s.isNotEmpty())
582                 trimmed = s.replaceCharacters ("\r\n\t", "   ");
583 
584             return trimmed;
585         }
586 
587         OwnedArray<Token> tokens;
588         int totalLines = 0;
589 
590         JUCE_DECLARE_NON_COPYABLE (TokenList)
591     };
592 }
593 
594 //==============================================================================
createStandardLayout(const AttributedString & text)595 void TextLayout::createStandardLayout (const AttributedString& text)
596 {
597     TextLayoutHelpers::TokenList l;
598     l.createLayout (text, *this);
599 }
600 
recalculateSize()601 void TextLayout::recalculateSize()
602 {
603     if (! lines.isEmpty())
604     {
605         auto bounds = lines.getFirst()->getLineBounds();
606 
607         for (auto* line : lines)
608             bounds = bounds.getUnion (line->getLineBounds());
609 
610         for (auto* line : lines)
611             line->lineOrigin.x -= bounds.getX();
612 
613         width  = bounds.getWidth();
614         height = bounds.getHeight();
615     }
616     else
617     {
618         width = 0;
619         height = 0;
620     }
621 }
622 
623 } // namespace juce
624