JUCE-7.0.12-0-g4f43011b96 JUCE-7.0.12-0-g4f43011b96
JUCE — C++ application framework with suport for VST, VST3, LV2 audio plug-ins

« « « Anklang Documentation
Loading...
Searching...
No Matches
juce_TextLayout.cpp
Go to the documentation of this file.
1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - 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 7 End-User License
11 Agreement and JUCE Privacy Policy.
12
13 End User License Agreement: www.juce.com/juce-7-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
26namespace juce
27{
28
29static String substring (const String& text, Range<int> range)
30{
31 return text.substring (range.getStart(), range.getEnd());
32}
33
34TextLayout::Glyph::Glyph (int glyph, Point<float> anch, float w) noexcept
35 : glyphCode (glyph), anchor (anch), width (w)
36{
37}
38
39//==============================================================================
40TextLayout::Run::Run (Range<int> range, int numGlyphsToPreallocate)
41 : stringRange (range)
42{
43 glyphs.ensureStorageAllocated (numGlyphsToPreallocate);
44}
45
46Range<float> TextLayout::Run::getRunBoundsX() const noexcept
47{
48 Range<float> range;
49 bool isFirst = true;
50
51 for (auto& glyph : glyphs)
52 {
53 Range<float> r (glyph.anchor.x, glyph.anchor.x + glyph.width);
54
55 if (isFirst)
56 {
57 isFirst = false;
58 range = r;
59 }
60 else
61 {
62 range = range.getUnionWith (r);
63 }
64 }
65
66 return range;
67}
68
69//==============================================================================
70TextLayout::Line::Line (Range<int> range, Point<float> o, float asc, float desc,
71 float lead, int numRunsToPreallocate)
72 : stringRange (range), lineOrigin (o),
73 ascent (asc), descent (desc), leading (lead)
74{
75 runs.ensureStorageAllocated (numRunsToPreallocate);
76}
77
78TextLayout::Line::Line (const Line& other)
79 : stringRange (other.stringRange), lineOrigin (other.lineOrigin),
80 ascent (other.ascent), descent (other.descent), leading (other.leading)
81{
82 runs.addCopiesOf (other.runs);
83}
84
85TextLayout::Line& TextLayout::Line::operator= (const Line& other)
86{
87 auto copy = other;
88 swap (copy);
89 return *this;
90}
91
93{
94 Range<float> range;
95 bool isFirst = true;
96
97 for (auto* run : runs)
98 {
99 auto runRange = run->getRunBoundsX();
100
101 if (isFirst)
102 {
103 isFirst = false;
104 range = runRange;
105 }
106 else
107 {
108 range = range.getUnionWith (runRange);
109 }
110 }
111
112 return range + lineOrigin.x;
113}
114
116{
117 return { lineOrigin.y - ascent,
118 lineOrigin.y + descent };
119}
120
122{
123 auto x = getLineBoundsX();
124 auto y = getLineBoundsY();
125
126 return { x.getStart(), y.getStart(), x.getLength(), y.getLength() };
127}
128
129void TextLayout::Line::swap (Line& other) noexcept
130{
131 std::swap (other.runs, runs);
132 std::swap (other.stringRange, stringRange);
133 std::swap (other.lineOrigin, lineOrigin);
134 std::swap (other.ascent, ascent);
135 std::swap (other.descent, descent);
136 std::swap (other.leading, leading);
137}
138
139//==============================================================================
141 : width (0), height (0), justification (Justification::topLeft)
142{
143}
144
146 : width (other.width), height (other.height),
147 justification (other.justification)
148{
149 lines.addCopiesOf (other.lines);
150}
151
152TextLayout::TextLayout (TextLayout&& other) noexcept
153 : lines (std::move (other.lines)),
154 width (other.width), height (other.height),
155 justification (other.justification)
156{
157}
158
159TextLayout& TextLayout::operator= (TextLayout&& other) noexcept
160{
161 lines = std::move (other.lines);
162 width = other.width;
163 height = other.height;
164 justification = other.justification;
165 return *this;
166}
167
168TextLayout& TextLayout::operator= (const TextLayout& other)
169{
170 width = other.width;
171 height = other.height;
172 justification = other.justification;
173 lines.clear();
174 lines.addCopiesOf (other.lines);
175 return *this;
176}
177
181
182TextLayout::Line& TextLayout::getLine (int index) const noexcept
183{
184 return *lines.getUnchecked (index);
185}
186
188{
189 lines.ensureStorageAllocated (numLinesNeeded);
190}
191
193{
194 lines.add (line.release());
195}
196
198{
199 auto origin = justification.appliedToRectangle (Rectangle<float> (width, getHeight()), area).getPosition();
200
201 auto& context = g.getInternalContext();
202 context.saveState();
203
204 auto clip = context.getClipBounds();
205 auto clipTop = (float) clip.getY() - origin.y;
206 auto clipBottom = (float) clip.getBottom() - origin.y;
207
208 for (auto& line : *this)
209 {
210 auto lineRangeY = line.getLineBoundsY();
211
212 if (lineRangeY.getEnd() < clipTop)
213 continue;
214
215 if (lineRangeY.getStart() > clipBottom)
216 break;
217
218 auto lineOrigin = origin + line.lineOrigin;
219
220 for (auto* run : line.runs)
221 {
222 context.setFont (run->font);
223 context.setFill (run->colour);
224
225 for (auto& glyph : run->glyphs)
226 context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x,
227 lineOrigin.y + glyph.anchor.y));
228
229 if (run->font.isUnderlined())
230 {
231 auto runExtent = run->getRunBoundsX();
232 auto lineThickness = run->font.getDescent() * 0.3f;
233
234 context.fillRect ({ runExtent.getStart() + lineOrigin.x, lineOrigin.y + lineThickness * 2.0f,
235 runExtent.getLength(), lineThickness });
236 }
237 }
238 }
239
240 context.restoreState();
241}
242
243void TextLayout::createLayout (const AttributedString& text, float maxWidth)
244{
245 createLayout (text, maxWidth, 1.0e7f);
246}
247
248void TextLayout::createLayout (const AttributedString& text, float maxWidth, float maxHeight)
249{
250 lines.clear();
251 width = maxWidth;
252 height = maxHeight;
253 justification = text.getJustification();
254
255 if (! createNativeLayout (text))
256 createStandardLayout (text);
257
259}
260
262{
263 createLayoutWithBalancedLineLengths (text, maxWidth, 1.0e7f);
264}
265
266void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth, float maxHeight)
267{
268 auto minimumWidth = maxWidth / 2.0f;
269 auto bestWidth = maxWidth;
270 float bestLineProportion = 0.0f;
271
272 while (maxWidth > minimumWidth)
273 {
274 createLayout (text, maxWidth, maxHeight);
275
276 if (getNumLines() < 2)
277 return;
278
279 auto line1 = lines.getUnchecked (lines.size() - 1)->getLineBoundsX().getLength();
280 auto line2 = lines.getUnchecked (lines.size() - 2)->getLineBoundsX().getLength();
281 auto shortest = jmin (line1, line2);
282 auto longest = jmax (line1, line2);
283 auto prop = shortest > 0 ? longest / shortest : 1.0f;
284
285 if (prop > 0.9f && prop < 1.1f)
286 return;
287
289 {
291 bestWidth = maxWidth;
292 }
293
294 maxWidth -= 10.0f;
295 }
296
297 if (! approximatelyEqual (bestWidth, maxWidth))
298 createLayout (text, bestWidth, maxHeight);
299}
300
301//==============================================================================
302namespace TextLayoutHelpers
303{
304 struct Token
305 {
306 Token (const String& t, const Font& f, Colour c, bool whitespace)
307 : text (t), font (f), colour (c),
308 area (font.getStringWidthFloat (t), f.getHeight()),
309 isWhitespace (whitespace),
310 isNewLine (t.containsChar ('\n') || t.containsChar ('\r'))
311 {}
312
313 const String text;
314 const Font font;
315 const Colour colour;
316 Rectangle<float> area;
317 int line;
318 float lineHeight;
319 const bool isWhitespace, isNewLine;
320
321 Token& operator= (const Token&) = delete;
322 };
323
325 {
326 TokenList() noexcept {}
327
328 void createLayout (const AttributedString& text, TextLayout& layout)
329 {
330 layout.ensureStorageAllocated (totalLines);
331
332 addTextRuns (text);
333 layoutRuns (layout.getWidth(), text.getLineSpacing(), text.getWordWrap());
334
335 int charPosition = 0;
336 int lineStartPosition = 0;
337 int runStartPosition = 0;
338
341
342 bool needToSetLineOrigin = true;
343
344 for (int i = 0; i < tokens.size(); ++i)
345 {
346 auto& t = *tokens.getUnchecked (i);
347
350 t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets);
351
352 if (currentRun == nullptr) currentRun = std::make_unique<TextLayout::Run>();
353 if (currentLine == nullptr) currentLine = std::make_unique<TextLayout::Line>();
354
355 const auto numGlyphs = newGlyphs.size();
357
358 if (numGlyphs > 0
359 && (! (t.isWhitespace || t.isNewLine) || needToSetLineOrigin))
360 {
361 currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size());
362 auto tokenOrigin = t.area.getPosition().translated (0, t.font.getAscent());
363
365 {
366 needToSetLineOrigin = false;
367 currentLine->lineOrigin = tokenOrigin;
368 }
369
370 auto glyphOffset = tokenOrigin - currentLine->lineOrigin;
371
372 for (int j = 0; j < newGlyphs.size(); ++j)
373 {
374 auto x = xOffsets.getUnchecked (j);
375 currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked (j),
376 glyphOffset.translated (x, 0),
377 xOffsets.getUnchecked (j + 1) - x));
378 }
379 }
380
381 if (auto* nextToken = tokens[i + 1])
382 {
383 if (t.font != nextToken->font || t.colour != nextToken->colour)
384 {
385 addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
387 }
388
389 if (t.line != nextToken->line)
390 {
391 if (currentRun == nullptr)
392 currentRun = std::make_unique<TextLayout::Run>();
393
394 addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
395 currentLine->stringRange = { lineStartPosition, charPosition };
396
398 layout.addLine (std::move (currentLine));
399
402 needToSetLineOrigin = true;
403 }
404 }
405 else
406 {
407 addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
408 currentLine->stringRange = { lineStartPosition, charPosition };
409
411 layout.addLine (std::move (currentLine));
412
413 needToSetLineOrigin = true;
414 }
415 }
416
418 {
419 auto totalW = layout.getWidth();
421
422 for (auto& line : layout)
423 {
424 auto dx = totalW - line.getLineBoundsX().getLength();
425
426 if (isCentred)
427 dx /= 2.0f;
428
429 line.lineOrigin.x += dx;
430 }
431 }
432 }
433
434 private:
435 static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun,
436 const Token& t, int start, int end)
437 {
438 glyphRun->stringRange = { start, end };
439 glyphRun->font = t.font;
440 glyphRun->colour = t.colour;
441 glyphLine.ascent = jmax (glyphLine.ascent, t.font.getAscent());
442 glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent());
443 glyphLine.runs.add (glyphRun);
444 }
445
446 static int getCharacterType (juce_wchar c) noexcept
447 {
448 if (c == '\r' || c == '\n')
449 return 0;
450
451 return CharacterFunctions::isWhitespace (c) ? 2 : 1;
452 }
453
454 void appendText (const String& stringText, const Font& font, Colour colour)
455 {
456 auto t = stringText.getCharPointer();
458 int lastCharType = 0;
459
460 for (;;)
461 {
462 auto c = t.getAndAdvance();
463
464 if (c == 0)
465 break;
466
467 auto charType = getCharacterType (c);
468
469 if (charType == 0 || charType != lastCharType)
470 {
471 if (currentString.isNotEmpty())
472 tokens.add (new Token (currentString, font, colour,
473 lastCharType == 2 || lastCharType == 0));
474
476
477 if (c == '\r' && *t == '\n')
478 currentString += t.getAndAdvance();
479 }
480 else
481 {
482 currentString += c;
483 }
484
486 }
487
488 if (currentString.isNotEmpty())
489 tokens.add (new Token (currentString, font, colour, lastCharType == 2));
490 }
491
492 void layoutRuns (float maxWidth, float extraLineSpacing, AttributedString::WordWrap wordWrap)
493 {
494 float x = 0, y = 0, h = 0;
495 int i;
496
497 for (i = 0; i < tokens.size(); ++i)
498 {
499 auto& t = *tokens.getUnchecked (i);
500 t.area.setPosition (x, y);
501 t.line = totalLines;
502 x += t.area.getWidth();
503 h = jmax (h, t.area.getHeight() + extraLineSpacing);
504
505 auto* nextTok = tokens[i + 1];
506
507 if (nextTok == nullptr)
508 break;
509
510 bool tokenTooLarge = (x + nextTok->area.getWidth() > maxWidth);
511
512 if (t.isNewLine || ((! nextTok->isWhitespace) && (tokenTooLarge && wordWrap != AttributedString::none)))
513 {
514 setLastLineHeight (i + 1, h);
515 x = 0;
516 y += h;
517 h = 0;
518 ++totalLines;
519 }
520 }
521
522 setLastLineHeight (jmin (i + 1, tokens.size()), h);
523 ++totalLines;
524 }
525
526 void setLastLineHeight (int i, float height) noexcept
527 {
528 while (--i >= 0)
529 {
530 auto& tok = *tokens.getUnchecked (i);
531
532 if (tok.line == totalLines)
533 tok.lineHeight = height;
534 else
535 break;
536 }
537 }
538
539 void addTextRuns (const AttributedString& text)
540 {
541 auto numAttributes = text.getNumAttributes();
542 tokens.ensureStorageAllocated (jmax (64, numAttributes));
543
544 for (int i = 0; i < numAttributes; ++i)
545 {
546 auto& attr = text.getAttribute (i);
547
548 appendText (substring (text.getText(), attr.range),
549 attr.font, attr.colour);
550 }
551 }
552
553 static String getTrimmedEndIfNotAllWhitespace (const String& s)
554 {
555 auto trimmed = s.trimEnd();
556
557 if (trimmed.isEmpty() && s.isNotEmpty())
558 trimmed = s.replaceCharacters ("\r\n\t", " ");
559
560 return trimmed;
561 }
562
563 OwnedArray<Token> tokens;
564 int totalLines = 0;
565
567 };
568}
569
570//==============================================================================
571void TextLayout::createStandardLayout (const AttributedString& text)
572{
574 l.createLayout (text, *this);
575}
576
578{
579 if (! lines.isEmpty())
580 {
581 auto bounds = lines.getFirst()->getLineBounds();
582
583 for (auto* line : lines)
584 bounds = bounds.getUnion (line->getLineBounds());
585
586 for (auto* line : lines)
587 line->lineOrigin.x -= bounds.getX();
588
589 width = bounds.getWidth();
590 height = bounds.getHeight();
591 }
592 else
593 {
594 width = 0;
595 height = 0;
596 }
597}
598
599} // namespace juce
static AffineTransform translation(float deltaX, float deltaY) noexcept
Returns a new transform which is a translation.
Holds a resizable array of primitive or copy-by-value objects.
Definition juce_Array.h:56
A text string with a set of colour/font settings that are associated with sub-ranges of the text.
WordWrap getWordWrap() const noexcept
Returns the word-wrapping behaviour.
float getLineSpacing() const noexcept
Returns the extra line-spacing distance.
const Attribute & getAttribute(int index) const noexcept
Returns one of the string's attributes.
int getNumAttributes() const noexcept
Returns the number of attributes that have been added to this string.
WordWrap
Types of word-wrap behaviour.
@ none
No word-wrapping: lines extend indefinitely.
const String & getText() const noexcept
Returns the complete text of this attributed string.
Justification getJustification() const noexcept
Returns the justification that should be used for laying-out the text.
static bool isWhitespace(char character) noexcept
Checks whether a character is whitespace.
Represents a colour, also including a transparency value.
Definition juce_Colour.h:38
Represents a particular font, including its size, style, etc.
Definition juce_Font.h:42
float getDescent() const
Returns the amount that the font descends below its baseline, in pixels.
float getHeight() const noexcept
Returns the total height of this font, in pixels.
float getAscent() const
Returns the height of the font above its baseline, in pixels.
A graphics context, used for drawing a component or image.
Represents a type of justification to be used when positioning graphical items.
@ horizontallyCentred
Indicates that the item should be placed in the centre between the left and right sides of the availa...
@ right
Indicates that the item should be aligned against the right edge of the available space.
const Rectangle< ValueType > appliedToRectangle(const Rectangle< ValueType > &areaToAdjust, const Rectangle< ValueType > &targetSpace) const noexcept
Returns the new position of a rectangle that has been justified to fit within a given space.
int getFlags() const noexcept
Returns the raw flags that are set for this Justification object.
Represents a line.
Definition juce_Line.h:47
An array designed for holding objects.
A pair of (x, y) coordinates.
Definition juce_Point.h:42
A general-purpose range object, that simply represents any linear range with a start and end point.
Definition juce_Range.h:40
constexpr Range getUnionWith(Range other) const noexcept
Returns the smallest range that contains both this one and the other one.
Definition juce_Range.h:246
Manages a rectangle and allows geometric operations to be performed on it.
ValueType getWidth() const noexcept
Returns the width of the rectangle.
void setPosition(Point< ValueType > newPos) noexcept
Changes the position of the rectangle's top-left corner (leaving its size unchanged).
ValueType getHeight() const noexcept
Returns the height of the rectangle.
The JUCE String class!
Definition juce_String.h:53
bool containsChar(juce_wchar character) const noexcept
Tests whether the string contains a particular character.
String trimEnd() const
Returns a copy of this string with any whitespace characters removed from the end.
String replaceCharacters(StringRef charactersToReplace, StringRef charactersToInsertInstead) const
Replaces a set of characters with another set.
static String charToString(juce_wchar character)
Creates a string from a single character.
String substring(int startIndex, int endIndex) const
Returns a subsection of the string.
bool isNotEmpty() const noexcept
Returns true if the string contains at least one character.
A positioned glyph.
A line containing a sequence of glyph-runs.
Range< float > getLineBoundsY() const noexcept
Returns the Y position range which contains all the glyphs in this line.
Rectangle< float > getLineBounds() const noexcept
Returns the smallest rectangle which contains all the glyphs in this line.
Range< float > getLineBoundsX() const noexcept
Returns the X position range which contains all the glyphs in this line.
A sequence of glyphs with a common font and colour.
A Pre-formatted piece of text, which may contain multiple fonts and colours.
~TextLayout()
Destructor.
void recalculateSize()
If you modify the TextLayout after creating it, call this to compute the new dimensions of the conten...
void createLayoutWithBalancedLineLengths(const AttributedString &, float maxWidth)
Creates a layout, attempting to choose a width which results in lines of a similar length.
int getNumLines() const noexcept
Returns the number of lines in the layout.
float getWidth() const noexcept
Returns the maximum width of the content.
void ensureStorageAllocated(int numLinesNeeded)
Pre-allocates space for the specified number of lines.
void addLine(std::unique_ptr< Line >)
Adds a line to the layout.
void draw(Graphics &, Rectangle< float > area) const
Draws the layout within the specified area.
float getHeight() const noexcept
Returns the maximum height of the content.
void createLayout(const AttributedString &, float maxWidth)
Creates a layout from the given attributed string.
TextLayout()
Creates an empty layout.
Line & getLine(int index) const noexcept
Returns one of the lines.
#define JUCE_DECLARE_NON_COPYABLE(className)
This is a shorthand macro for deleting a class's copy constructor and copy assignment operator.
typedef float
JUCE Namespace.
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.
RangedDirectoryIterator end(const RangedDirectoryIterator &)
Returns a default-constructed sentinel value.
Type unalignedPointerCast(void *ptr) noexcept
Casts a pointer to another type via void*, which suppresses the cast-align warning which sometimes ar...
Definition juce_Memory.h:88
T release(T... args)
T swap(T... args)