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