Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

juce_TextLayout.cpp 21KB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2013 - Raw Material Software Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. TextLayout::Glyph::Glyph (const int glyph, Point<float> anch, float w) noexcept
  18. : glyphCode (glyph), anchor (anch), width (w)
  19. {
  20. }
  21. TextLayout::Glyph::Glyph (const Glyph& other) noexcept
  22. : glyphCode (other.glyphCode), anchor (other.anchor), width (other.width)
  23. {
  24. }
  25. TextLayout::Glyph& TextLayout::Glyph::operator= (const Glyph& other) noexcept
  26. {
  27. glyphCode = other.glyphCode;
  28. anchor = other.anchor;
  29. width = other.width;
  30. return *this;
  31. }
  32. TextLayout::Glyph::~Glyph() noexcept {}
  33. //==============================================================================
  34. TextLayout::Run::Run() noexcept
  35. : colour (0xff000000)
  36. {
  37. }
  38. TextLayout::Run::Run (Range<int> range, const int numGlyphsToPreallocate)
  39. : colour (0xff000000), stringRange (range)
  40. {
  41. glyphs.ensureStorageAllocated (numGlyphsToPreallocate);
  42. }
  43. TextLayout::Run::Run (const Run& other)
  44. : font (other.font),
  45. colour (other.colour),
  46. glyphs (other.glyphs),
  47. stringRange (other.stringRange)
  48. {
  49. }
  50. TextLayout::Run::~Run() noexcept {}
  51. //==============================================================================
  52. TextLayout::Line::Line() noexcept
  53. : ascent (0.0f), descent (0.0f), leading (0.0f)
  54. {
  55. }
  56. TextLayout::Line::Line (Range<int> range, Point<float> o, float asc, float desc,
  57. float lead, int numRunsToPreallocate)
  58. : stringRange (range), lineOrigin (o),
  59. ascent (asc), descent (desc), leading (lead)
  60. {
  61. runs.ensureStorageAllocated (numRunsToPreallocate);
  62. }
  63. TextLayout::Line::Line (const Line& other)
  64. : stringRange (other.stringRange), lineOrigin (other.lineOrigin),
  65. ascent (other.ascent), descent (other.descent), leading (other.leading)
  66. {
  67. runs.addCopiesOf (other.runs);
  68. }
  69. TextLayout::Line::~Line() noexcept
  70. {
  71. }
  72. Range<float> TextLayout::Line::getLineBoundsX() const noexcept
  73. {
  74. Range<float> range;
  75. bool isFirst = true;
  76. for (int i = runs.size(); --i >= 0;)
  77. {
  78. const Run& run = *runs.getUnchecked(i);
  79. if (run.glyphs.size() > 0)
  80. {
  81. float minX = run.glyphs.getReference(0).anchor.x;
  82. float maxX = minX;
  83. for (int j = run.glyphs.size(); --j >= 0;)
  84. {
  85. const Glyph& glyph = run.glyphs.getReference (j);
  86. const float x = glyph.anchor.x;
  87. minX = jmin (minX, x);
  88. maxX = jmax (maxX, x + glyph.width);
  89. }
  90. if (isFirst)
  91. {
  92. isFirst = false;
  93. range = Range<float> (minX, maxX);
  94. }
  95. else
  96. {
  97. range = range.getUnionWith (Range<float> (minX, maxX));
  98. }
  99. }
  100. }
  101. return range + lineOrigin.x;
  102. }
  103. Range<float> TextLayout::Line::getLineBoundsY() const noexcept
  104. {
  105. return Range<float> (lineOrigin.y - ascent,
  106. lineOrigin.y + descent);
  107. }
  108. Rectangle<float> TextLayout::Line::getLineBounds() const noexcept
  109. {
  110. const Range<float> x (getLineBoundsX()),
  111. y (getLineBoundsY());
  112. return Rectangle<float> (x.getStart(), y.getStart(), x.getLength(), y.getLength());
  113. }
  114. //==============================================================================
  115. TextLayout::TextLayout()
  116. : width (0), height (0), justification (Justification::topLeft)
  117. {
  118. }
  119. TextLayout::TextLayout (const TextLayout& other)
  120. : width (other.width), height (other.height),
  121. justification (other.justification)
  122. {
  123. lines.addCopiesOf (other.lines);
  124. }
  125. #if JUCE_COMPILER_SUPPORTS_MOVE_SEMANTICS
  126. TextLayout::TextLayout (TextLayout&& other) noexcept
  127. : lines (static_cast<OwnedArray<Line>&&> (other.lines)),
  128. width (other.width), height (other.height),
  129. justification (other.justification)
  130. {
  131. }
  132. TextLayout& TextLayout::operator= (TextLayout&& other) noexcept
  133. {
  134. lines = static_cast<OwnedArray<Line>&&> (other.lines);
  135. width = other.width;
  136. height = other.height;
  137. justification = other.justification;
  138. return *this;
  139. }
  140. #endif
  141. TextLayout& TextLayout::operator= (const TextLayout& other)
  142. {
  143. width = other.width;
  144. height = other.height;
  145. justification = other.justification;
  146. lines.clear();
  147. lines.addCopiesOf (other.lines);
  148. return *this;
  149. }
  150. TextLayout::~TextLayout()
  151. {
  152. }
  153. TextLayout::Line& TextLayout::getLine (const int index) const
  154. {
  155. return *lines.getUnchecked (index);
  156. }
  157. void TextLayout::ensureStorageAllocated (int numLinesNeeded)
  158. {
  159. lines.ensureStorageAllocated (numLinesNeeded);
  160. }
  161. void TextLayout::addLine (Line* line)
  162. {
  163. lines.add (line);
  164. }
  165. void TextLayout::draw (Graphics& g, const Rectangle<float>& area) const
  166. {
  167. const Point<float> origin (justification.appliedToRectangle (Rectangle<float> (width, getHeight()), area).getPosition());
  168. LowLevelGraphicsContext& context = g.getInternalContext();
  169. for (int i = 0; i < lines.size(); ++i)
  170. {
  171. const Line& line = getLine (i);
  172. const Point<float> lineOrigin (origin + line.lineOrigin);
  173. for (int j = 0; j < line.runs.size(); ++j)
  174. {
  175. const Run& run = *line.runs.getUnchecked (j);
  176. context.setFont (run.font);
  177. context.setFill (run.colour);
  178. for (int k = 0; k < run.glyphs.size(); ++k)
  179. {
  180. const Glyph& glyph = run.glyphs.getReference (k);
  181. context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x,
  182. lineOrigin.y + glyph.anchor.y));
  183. }
  184. }
  185. }
  186. }
  187. void TextLayout::createLayout (const AttributedString& text, float maxWidth)
  188. {
  189. createLayout (text, maxWidth, 1.0e7f);
  190. }
  191. void TextLayout::createLayout (const AttributedString& text, float maxWidth, float maxHeight)
  192. {
  193. lines.clear();
  194. width = maxWidth;
  195. height = maxHeight;
  196. justification = text.getJustification();
  197. if (! createNativeLayout (text))
  198. createStandardLayout (text);
  199. recalculateSize (text);
  200. }
  201. void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth)
  202. {
  203. createLayoutWithBalancedLineLengths (text, maxWidth, 1.0e7f);
  204. }
  205. void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth, float maxHeight)
  206. {
  207. const float minimumWidth = maxWidth / 2.0f;
  208. float bestWidth = maxWidth;
  209. float bestLineProportion = 0.0f;
  210. while (maxWidth > minimumWidth)
  211. {
  212. createLayout (text, maxWidth, maxHeight);
  213. if (getNumLines() < 2)
  214. return;
  215. const float line1 = lines.getUnchecked (lines.size() - 1)->getLineBoundsX().getLength();
  216. const float line2 = lines.getUnchecked (lines.size() - 2)->getLineBoundsX().getLength();
  217. const float shortestLine = jmin (line1, line2);
  218. const float prop = (shortestLine > 0) ? jmax (line1, line2) / shortestLine : 1.0f;
  219. if (prop > 0.9f)
  220. return;
  221. if (prop > bestLineProportion)
  222. {
  223. bestLineProportion = prop;
  224. bestWidth = maxWidth;
  225. }
  226. maxWidth -= 10.0f;
  227. }
  228. if (bestWidth != maxWidth)
  229. createLayout (text, bestWidth, maxHeight);
  230. }
  231. //==============================================================================
  232. namespace TextLayoutHelpers
  233. {
  234. struct FontAndColour
  235. {
  236. FontAndColour (const Font* f) noexcept : font (f), colour (0xff000000) {}
  237. const Font* font;
  238. Colour colour;
  239. bool operator!= (const FontAndColour& other) const noexcept
  240. {
  241. return (font != other.font && *font != *other.font) || colour != other.colour;
  242. }
  243. };
  244. struct RunAttribute
  245. {
  246. RunAttribute (const FontAndColour& fc, const Range<int> r) noexcept
  247. : fontAndColour (fc), range (r)
  248. {}
  249. FontAndColour fontAndColour;
  250. Range<int> range;
  251. };
  252. struct Token
  253. {
  254. Token (const String& t, const Font& f, Colour c, const bool whitespace)
  255. : text (t), font (f), colour (c),
  256. area (font.getStringWidthFloat (t), f.getHeight()),
  257. isWhitespace (whitespace),
  258. isNewLine (t.containsChar ('\n') || t.containsChar ('\r'))
  259. {}
  260. const String text;
  261. const Font font;
  262. const Colour colour;
  263. Rectangle<float> area;
  264. int line;
  265. float lineHeight;
  266. const bool isWhitespace, isNewLine;
  267. private:
  268. Token& operator= (const Token&);
  269. };
  270. class TokenList
  271. {
  272. public:
  273. TokenList() noexcept : totalLines (0) {}
  274. void createLayout (const AttributedString& text, TextLayout& layout)
  275. {
  276. tokens.ensureStorageAllocated (64);
  277. layout.ensureStorageAllocated (totalLines);
  278. addTextRuns (text);
  279. layoutRuns (layout.getWidth());
  280. int charPosition = 0;
  281. int lineStartPosition = 0;
  282. int runStartPosition = 0;
  283. ScopedPointer<TextLayout::Line> currentLine;
  284. ScopedPointer<TextLayout::Run> currentRun;
  285. bool needToSetLineOrigin = true;
  286. for (int i = 0; i < tokens.size(); ++i)
  287. {
  288. const Token& t = *tokens.getUnchecked (i);
  289. Array<int> newGlyphs;
  290. Array<float> xOffsets;
  291. t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets);
  292. if (currentRun == nullptr) currentRun = new TextLayout::Run();
  293. if (currentLine == nullptr) currentLine = new TextLayout::Line();
  294. if (newGlyphs.size() > 0)
  295. {
  296. currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size());
  297. const Point<float> tokenOrigin (t.area.getPosition().translated (0, t.font.getAscent()));
  298. if (needToSetLineOrigin)
  299. {
  300. needToSetLineOrigin = false;
  301. currentLine->lineOrigin = tokenOrigin;
  302. }
  303. const Point<float> glyphOffset (tokenOrigin - currentLine->lineOrigin);
  304. for (int j = 0; j < newGlyphs.size(); ++j)
  305. {
  306. const float x = xOffsets.getUnchecked (j);
  307. currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked(j),
  308. glyphOffset.translated (x, 0),
  309. xOffsets.getUnchecked (j + 1) - x));
  310. }
  311. charPosition += newGlyphs.size();
  312. }
  313. if (t.isWhitespace || t.isNewLine)
  314. ++charPosition;
  315. const Token* const nextToken = tokens [i + 1];
  316. if (nextToken == nullptr) // this is the last token
  317. {
  318. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  319. currentLine->stringRange = Range<int> (lineStartPosition, charPosition);
  320. if (! needToSetLineOrigin)
  321. layout.addLine (currentLine.release());
  322. needToSetLineOrigin = true;
  323. }
  324. else
  325. {
  326. if (t.font != nextToken->font || t.colour != nextToken->colour)
  327. {
  328. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  329. runStartPosition = charPosition;
  330. }
  331. if (t.line != nextToken->line)
  332. {
  333. if (currentRun == nullptr)
  334. currentRun = new TextLayout::Run();
  335. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  336. currentLine->stringRange = Range<int> (lineStartPosition, charPosition);
  337. if (! needToSetLineOrigin)
  338. layout.addLine (currentLine.release());
  339. runStartPosition = charPosition;
  340. lineStartPosition = charPosition;
  341. needToSetLineOrigin = true;
  342. }
  343. }
  344. }
  345. if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0)
  346. {
  347. const float totalW = layout.getWidth();
  348. const bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0;
  349. for (int i = 0; i < layout.getNumLines(); ++i)
  350. {
  351. float dx = totalW - layout.getLine(i).getLineBoundsX().getLength();
  352. if (isCentred)
  353. dx /= 2.0f;
  354. layout.getLine(i).lineOrigin.x += dx;
  355. }
  356. }
  357. }
  358. private:
  359. static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun,
  360. const Token& t, const int start, const int end)
  361. {
  362. glyphRun->stringRange = Range<int> (start, end);
  363. glyphRun->font = t.font;
  364. glyphRun->colour = t.colour;
  365. glyphLine.ascent = jmax (glyphLine.ascent, t.font.getAscent());
  366. glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent());
  367. glyphLine.runs.add (glyphRun);
  368. }
  369. static int getCharacterType (const juce_wchar c) noexcept
  370. {
  371. if (c == '\r' || c == '\n')
  372. return 0;
  373. return CharacterFunctions::isWhitespace (c) ? 2 : 1;
  374. }
  375. void appendText (const AttributedString& text, const Range<int> stringRange,
  376. const Font& font, Colour colour)
  377. {
  378. const String stringText (text.getText().substring (stringRange.getStart(), stringRange.getEnd()));
  379. String::CharPointerType t (stringText.getCharPointer());
  380. String currentString;
  381. int lastCharType = 0;
  382. for (;;)
  383. {
  384. const juce_wchar c = t.getAndAdvance();
  385. if (c == 0)
  386. break;
  387. const int charType = getCharacterType (c);
  388. if (charType == 0 || charType != lastCharType)
  389. {
  390. if (currentString.isNotEmpty())
  391. tokens.add (new Token (currentString, font, colour,
  392. lastCharType == 2 || lastCharType == 0));
  393. currentString = String::charToString (c);
  394. if (c == '\r' && *t == '\n')
  395. currentString += t.getAndAdvance();
  396. }
  397. else
  398. {
  399. currentString += c;
  400. }
  401. lastCharType = charType;
  402. }
  403. if (currentString.isNotEmpty())
  404. tokens.add (new Token (currentString, font, colour, lastCharType == 2));
  405. }
  406. void layoutRuns (const float maxWidth)
  407. {
  408. float x = 0, y = 0, h = 0;
  409. int i;
  410. for (i = 0; i < tokens.size(); ++i)
  411. {
  412. Token& t = *tokens.getUnchecked(i);
  413. t.area.setPosition (x, y);
  414. t.line = totalLines;
  415. x += t.area.getWidth();
  416. h = jmax (h, t.area.getHeight());
  417. const Token* const nextTok = tokens[i + 1];
  418. if (nextTok == nullptr)
  419. break;
  420. if (t.isNewLine || ((! nextTok->isWhitespace) && x + nextTok->area.getWidth() > maxWidth))
  421. {
  422. setLastLineHeight (i + 1, h);
  423. x = 0;
  424. y += h;
  425. h = 0;
  426. ++totalLines;
  427. }
  428. }
  429. setLastLineHeight (jmin (i + 1, tokens.size()), h);
  430. ++totalLines;
  431. }
  432. void setLastLineHeight (int i, const float height) noexcept
  433. {
  434. while (--i >= 0)
  435. {
  436. Token& tok = *tokens.getUnchecked (i);
  437. if (tok.line == totalLines)
  438. tok.lineHeight = height;
  439. else
  440. break;
  441. }
  442. }
  443. void addTextRuns (const AttributedString& text)
  444. {
  445. Font defaultFont;
  446. Array<RunAttribute> runAttributes;
  447. {
  448. const int stringLength = text.getText().length();
  449. int rangeStart = 0;
  450. FontAndColour lastFontAndColour (&defaultFont);
  451. // Iterate through every character in the string
  452. for (int i = 0; i < stringLength; ++i)
  453. {
  454. FontAndColour newFontAndColour (&defaultFont);
  455. const int numCharacterAttributes = text.getNumAttributes();
  456. for (int j = 0; j < numCharacterAttributes; ++j)
  457. {
  458. const AttributedString::Attribute& attr = *text.getAttribute (j);
  459. if (attr.range.contains (i))
  460. {
  461. if (const Font* f = attr.getFont()) newFontAndColour.font = f;
  462. if (const Colour* c = attr.getColour()) newFontAndColour.colour = *c;
  463. }
  464. }
  465. if (i > 0 && newFontAndColour != lastFontAndColour)
  466. {
  467. runAttributes.add (RunAttribute (lastFontAndColour, Range<int> (rangeStart, i)));
  468. rangeStart = i;
  469. }
  470. lastFontAndColour = newFontAndColour;
  471. }
  472. if (rangeStart < stringLength)
  473. runAttributes.add (RunAttribute (lastFontAndColour, Range<int> (rangeStart, stringLength)));
  474. }
  475. for (int i = 0; i < runAttributes.size(); ++i)
  476. {
  477. const RunAttribute& r = runAttributes.getReference(i);
  478. appendText (text, r.range, *(r.fontAndColour.font), r.fontAndColour.colour);
  479. }
  480. }
  481. static String getTrimmedEndIfNotAllWhitespace (const String& s)
  482. {
  483. String trimmed (s.trimEnd());
  484. if (trimmed.isEmpty() && ! s.isEmpty())
  485. trimmed = s.replaceCharacters ("\r\n\t", " ");
  486. return trimmed;
  487. }
  488. OwnedArray<Token> tokens;
  489. int totalLines;
  490. JUCE_DECLARE_NON_COPYABLE (TokenList)
  491. };
  492. }
  493. //==============================================================================
  494. void TextLayout::createStandardLayout (const AttributedString& text)
  495. {
  496. TextLayoutHelpers::TokenList l;
  497. l.createLayout (text, *this);
  498. }
  499. void TextLayout::recalculateSize (const AttributedString& text)
  500. {
  501. if (lines.size() > 0 && text.getReadingDirection() != AttributedString::rightToLeft)
  502. {
  503. Rectangle<float> bounds (lines.getFirst()->getLineBounds());
  504. for (int i = lines.size(); --i > 0;)
  505. bounds = bounds.getUnion (lines.getUnchecked(i)->getLineBounds());
  506. for (int i = lines.size(); --i >= 0;)
  507. lines.getUnchecked(i)->lineOrigin.x -= bounds.getX();
  508. width = bounds.getWidth();
  509. height = bounds.getHeight();
  510. }
  511. else
  512. {
  513. width = 0;
  514. height = 0;
  515. }
  516. }