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 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. static String substring (const String& text, Range<int> range)
  21. {
  22. return text.substring (range.getStart(), range.getEnd());
  23. }
  24. TextLayout::Glyph::Glyph (int glyph, Point<float> anch, float w) noexcept
  25. : glyphCode (glyph), anchor (anch), width (w)
  26. {
  27. }
  28. //==============================================================================
  29. TextLayout::Run::Run (Range<int> range, int numGlyphsToPreallocate)
  30. : stringRange (range)
  31. {
  32. glyphs.ensureStorageAllocated (numGlyphsToPreallocate);
  33. }
  34. Range<float> TextLayout::Run::getRunBoundsX() const noexcept
  35. {
  36. Range<float> range;
  37. bool isFirst = true;
  38. for (auto& glyph : glyphs)
  39. {
  40. Range<float> r (glyph.anchor.x, glyph.anchor.x + glyph.width);
  41. if (isFirst)
  42. {
  43. isFirst = false;
  44. range = r;
  45. }
  46. else
  47. {
  48. range = range.getUnionWith (r);
  49. }
  50. }
  51. return range;
  52. }
  53. //==============================================================================
  54. TextLayout::Line::Line (Range<int> range, Point<float> o, float asc, float desc,
  55. float lead, int numRunsToPreallocate)
  56. : stringRange (range), lineOrigin (o),
  57. ascent (asc), descent (desc), leading (lead)
  58. {
  59. runs.ensureStorageAllocated (numRunsToPreallocate);
  60. }
  61. TextLayout::Line::Line (const Line& other)
  62. : stringRange (other.stringRange), lineOrigin (other.lineOrigin),
  63. ascent (other.ascent), descent (other.descent), leading (other.leading)
  64. {
  65. runs.addCopiesOf (other.runs);
  66. }
  67. TextLayout::Line& TextLayout::Line::operator= (const Line& other)
  68. {
  69. auto copy = other;
  70. swap (copy);
  71. return *this;
  72. }
  73. Range<float> TextLayout::Line::getLineBoundsX() const noexcept
  74. {
  75. Range<float> range;
  76. bool isFirst = true;
  77. for (auto* run : runs)
  78. {
  79. auto runRange = run->getRunBoundsX();
  80. if (isFirst)
  81. {
  82. isFirst = false;
  83. range = runRange;
  84. }
  85. else
  86. {
  87. range = range.getUnionWith (runRange);
  88. }
  89. }
  90. return range + lineOrigin.x;
  91. }
  92. Range<float> TextLayout::Line::getLineBoundsY() const noexcept
  93. {
  94. return { lineOrigin.y - ascent,
  95. lineOrigin.y + descent };
  96. }
  97. Rectangle<float> TextLayout::Line::getLineBounds() const noexcept
  98. {
  99. auto x = getLineBoundsX();
  100. auto y = getLineBoundsY();
  101. return { x.getStart(), y.getStart(), x.getLength(), y.getLength() };
  102. }
  103. void TextLayout::Line::swap (Line& other) noexcept
  104. {
  105. std::swap (other.runs, runs);
  106. std::swap (other.stringRange, stringRange);
  107. std::swap (other.lineOrigin, lineOrigin);
  108. std::swap (other.ascent, ascent);
  109. std::swap (other.descent, descent);
  110. std::swap (other.leading, leading);
  111. }
  112. //==============================================================================
  113. TextLayout::TextLayout()
  114. : width (0), height (0), justification (Justification::topLeft)
  115. {
  116. }
  117. TextLayout::TextLayout (const TextLayout& other)
  118. : width (other.width), height (other.height),
  119. justification (other.justification)
  120. {
  121. lines.addCopiesOf (other.lines);
  122. }
  123. TextLayout::TextLayout (TextLayout&& other) noexcept
  124. : lines (std::move (other.lines)),
  125. width (other.width), height (other.height),
  126. justification (other.justification)
  127. {
  128. }
  129. TextLayout& TextLayout::operator= (TextLayout&& other) noexcept
  130. {
  131. lines = std::move (other.lines);
  132. width = other.width;
  133. height = other.height;
  134. justification = other.justification;
  135. return *this;
  136. }
  137. TextLayout& TextLayout::operator= (const TextLayout& other)
  138. {
  139. width = other.width;
  140. height = other.height;
  141. justification = other.justification;
  142. lines.clear();
  143. lines.addCopiesOf (other.lines);
  144. return *this;
  145. }
  146. TextLayout::~TextLayout()
  147. {
  148. }
  149. TextLayout::Line& TextLayout::getLine (int index) const noexcept
  150. {
  151. return *lines.getUnchecked (index);
  152. }
  153. void TextLayout::ensureStorageAllocated (int numLinesNeeded)
  154. {
  155. lines.ensureStorageAllocated (numLinesNeeded);
  156. }
  157. void TextLayout::addLine (std::unique_ptr<Line> line)
  158. {
  159. lines.add (line.release());
  160. }
  161. void TextLayout::draw (Graphics& g, Rectangle<float> area) const
  162. {
  163. auto origin = justification.appliedToRectangle (Rectangle<float> (width, getHeight()), area).getPosition();
  164. auto& context = g.getInternalContext();
  165. context.saveState();
  166. auto clip = context.getClipBounds();
  167. auto clipTop = (float) clip.getY() - origin.y;
  168. auto clipBottom = (float) clip.getBottom() - origin.y;
  169. for (auto& line : *this)
  170. {
  171. auto lineRangeY = line.getLineBoundsY();
  172. if (lineRangeY.getEnd() < clipTop)
  173. continue;
  174. if (lineRangeY.getStart() > clipBottom)
  175. break;
  176. auto lineOrigin = origin + line.lineOrigin;
  177. for (auto* run : line.runs)
  178. {
  179. context.setFont (run->font);
  180. context.setFill (run->colour);
  181. for (auto& glyph : run->glyphs)
  182. context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x,
  183. lineOrigin.y + glyph.anchor.y));
  184. if (run->font.isUnderlined())
  185. {
  186. auto runExtent = run->getRunBoundsX();
  187. auto lineThickness = run->font.getDescent() * 0.3f;
  188. context.fillRect ({ runExtent.getStart() + lineOrigin.x, lineOrigin.y + lineThickness * 2.0f,
  189. runExtent.getLength(), lineThickness });
  190. }
  191. }
  192. }
  193. context.restoreState();
  194. }
  195. void TextLayout::createLayout (const AttributedString& text, float maxWidth)
  196. {
  197. createLayout (text, maxWidth, 1.0e7f);
  198. }
  199. void TextLayout::createLayout (const AttributedString& text, float maxWidth, float maxHeight)
  200. {
  201. lines.clear();
  202. width = maxWidth;
  203. height = maxHeight;
  204. justification = text.getJustification();
  205. if (! createNativeLayout (text))
  206. createStandardLayout (text);
  207. recalculateSize();
  208. }
  209. void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth)
  210. {
  211. createLayoutWithBalancedLineLengths (text, maxWidth, 1.0e7f);
  212. }
  213. void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth, float maxHeight)
  214. {
  215. auto minimumWidth = maxWidth / 2.0f;
  216. auto bestWidth = maxWidth;
  217. float bestLineProportion = 0.0f;
  218. while (maxWidth > minimumWidth)
  219. {
  220. createLayout (text, maxWidth, maxHeight);
  221. if (getNumLines() < 2)
  222. return;
  223. auto line1 = lines.getUnchecked (lines.size() - 1)->getLineBoundsX().getLength();
  224. auto line2 = lines.getUnchecked (lines.size() - 2)->getLineBoundsX().getLength();
  225. auto shortest = jmin (line1, line2);
  226. auto longest = jmax (line1, line2);
  227. auto prop = shortest > 0 ? longest / shortest : 1.0f;
  228. if (prop > 0.9f && prop < 1.1f)
  229. return;
  230. if (prop > bestLineProportion)
  231. {
  232. bestLineProportion = prop;
  233. bestWidth = maxWidth;
  234. }
  235. maxWidth -= 10.0f;
  236. }
  237. if (bestWidth != maxWidth)
  238. createLayout (text, bestWidth, maxHeight);
  239. }
  240. //==============================================================================
  241. namespace TextLayoutHelpers
  242. {
  243. struct Token
  244. {
  245. Token (const String& t, const Font& f, Colour c, bool whitespace)
  246. : text (t), font (f), colour (c),
  247. area (font.getStringWidthFloat (t), f.getHeight()),
  248. isWhitespace (whitespace),
  249. isNewLine (t.containsChar ('\n') || t.containsChar ('\r'))
  250. {}
  251. const String text;
  252. const Font font;
  253. const Colour colour;
  254. Rectangle<float> area;
  255. int line;
  256. float lineHeight;
  257. const bool isWhitespace, isNewLine;
  258. Token& operator= (const Token&) = delete;
  259. };
  260. struct TokenList
  261. {
  262. TokenList() noexcept {}
  263. void createLayout (const AttributedString& text, TextLayout& layout)
  264. {
  265. layout.ensureStorageAllocated (totalLines);
  266. addTextRuns (text);
  267. layoutRuns (layout.getWidth(), text.getLineSpacing(), text.getWordWrap());
  268. int charPosition = 0;
  269. int lineStartPosition = 0;
  270. int runStartPosition = 0;
  271. std::unique_ptr<TextLayout::Line> currentLine;
  272. std::unique_ptr<TextLayout::Run> currentRun;
  273. bool needToSetLineOrigin = true;
  274. for (int i = 0; i < tokens.size(); ++i)
  275. {
  276. auto& t = *tokens.getUnchecked (i);
  277. Array<int> newGlyphs;
  278. Array<float> xOffsets;
  279. t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets);
  280. if (currentRun == nullptr) currentRun = std::make_unique<TextLayout::Run>();
  281. if (currentLine == nullptr) currentLine = std::make_unique<TextLayout::Line>();
  282. const auto numGlyphs = newGlyphs.size();
  283. charPosition += numGlyphs;
  284. if (numGlyphs > 0
  285. && (! (t.isWhitespace || t.isNewLine) || needToSetLineOrigin))
  286. {
  287. currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size());
  288. auto tokenOrigin = t.area.getPosition().translated (0, t.font.getAscent());
  289. if (needToSetLineOrigin)
  290. {
  291. needToSetLineOrigin = false;
  292. currentLine->lineOrigin = tokenOrigin;
  293. }
  294. auto glyphOffset = tokenOrigin - currentLine->lineOrigin;
  295. for (int j = 0; j < newGlyphs.size(); ++j)
  296. {
  297. auto x = xOffsets.getUnchecked (j);
  298. currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked (j),
  299. glyphOffset.translated (x, 0),
  300. xOffsets.getUnchecked (j + 1) - x));
  301. }
  302. }
  303. if (auto* nextToken = tokens[i + 1])
  304. {
  305. if (t.font != nextToken->font || t.colour != nextToken->colour)
  306. {
  307. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  308. runStartPosition = charPosition;
  309. }
  310. if (t.line != nextToken->line)
  311. {
  312. if (currentRun == nullptr)
  313. currentRun = std::make_unique<TextLayout::Run>();
  314. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  315. currentLine->stringRange = { lineStartPosition, charPosition };
  316. if (! needToSetLineOrigin)
  317. layout.addLine (std::move (currentLine));
  318. runStartPosition = charPosition;
  319. lineStartPosition = charPosition;
  320. needToSetLineOrigin = true;
  321. }
  322. }
  323. else
  324. {
  325. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  326. currentLine->stringRange = { lineStartPosition, charPosition };
  327. if (! needToSetLineOrigin)
  328. layout.addLine (std::move (currentLine));
  329. needToSetLineOrigin = true;
  330. }
  331. }
  332. if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0)
  333. {
  334. auto totalW = layout.getWidth();
  335. bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0;
  336. for (auto& line : layout)
  337. {
  338. auto dx = totalW - line.getLineBoundsX().getLength();
  339. if (isCentred)
  340. dx /= 2.0f;
  341. line.lineOrigin.x += dx;
  342. }
  343. }
  344. }
  345. private:
  346. static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun,
  347. const Token& t, int start, int end)
  348. {
  349. glyphRun->stringRange = { start, end };
  350. glyphRun->font = t.font;
  351. glyphRun->colour = t.colour;
  352. glyphLine.ascent = jmax (glyphLine.ascent, t.font.getAscent());
  353. glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent());
  354. glyphLine.runs.add (glyphRun);
  355. }
  356. static int getCharacterType (juce_wchar c) noexcept
  357. {
  358. if (c == '\r' || c == '\n')
  359. return 0;
  360. return CharacterFunctions::isWhitespace (c) ? 2 : 1;
  361. }
  362. void appendText (const String& stringText, const Font& font, Colour colour)
  363. {
  364. auto t = stringText.getCharPointer();
  365. String currentString;
  366. int lastCharType = 0;
  367. for (;;)
  368. {
  369. auto c = t.getAndAdvance();
  370. if (c == 0)
  371. break;
  372. auto charType = getCharacterType (c);
  373. if (charType == 0 || charType != lastCharType)
  374. {
  375. if (currentString.isNotEmpty())
  376. tokens.add (new Token (currentString, font, colour,
  377. lastCharType == 2 || lastCharType == 0));
  378. currentString = String::charToString (c);
  379. if (c == '\r' && *t == '\n')
  380. currentString += t.getAndAdvance();
  381. }
  382. else
  383. {
  384. currentString += c;
  385. }
  386. lastCharType = charType;
  387. }
  388. if (currentString.isNotEmpty())
  389. tokens.add (new Token (currentString, font, colour, lastCharType == 2));
  390. }
  391. void layoutRuns (float maxWidth, float extraLineSpacing, AttributedString::WordWrap wordWrap)
  392. {
  393. float x = 0, y = 0, h = 0;
  394. int i;
  395. for (i = 0; i < tokens.size(); ++i)
  396. {
  397. auto& t = *tokens.getUnchecked (i);
  398. t.area.setPosition (x, y);
  399. t.line = totalLines;
  400. x += t.area.getWidth();
  401. h = jmax (h, t.area.getHeight() + extraLineSpacing);
  402. auto* nextTok = tokens[i + 1];
  403. if (nextTok == nullptr)
  404. break;
  405. bool tokenTooLarge = (x + nextTok->area.getWidth() > maxWidth);
  406. if (t.isNewLine || ((! nextTok->isWhitespace) && (tokenTooLarge && wordWrap != AttributedString::none)))
  407. {
  408. setLastLineHeight (i + 1, h);
  409. x = 0;
  410. y += h;
  411. h = 0;
  412. ++totalLines;
  413. }
  414. }
  415. setLastLineHeight (jmin (i + 1, tokens.size()), h);
  416. ++totalLines;
  417. }
  418. void setLastLineHeight (int i, float height) noexcept
  419. {
  420. while (--i >= 0)
  421. {
  422. auto& tok = *tokens.getUnchecked (i);
  423. if (tok.line == totalLines)
  424. tok.lineHeight = height;
  425. else
  426. break;
  427. }
  428. }
  429. void addTextRuns (const AttributedString& text)
  430. {
  431. auto numAttributes = text.getNumAttributes();
  432. tokens.ensureStorageAllocated (jmax (64, numAttributes));
  433. for (int i = 0; i < numAttributes; ++i)
  434. {
  435. auto& attr = text.getAttribute (i);
  436. appendText (substring (text.getText(), attr.range),
  437. attr.font, attr.colour);
  438. }
  439. }
  440. static String getTrimmedEndIfNotAllWhitespace (const String& s)
  441. {
  442. auto trimmed = s.trimEnd();
  443. if (trimmed.isEmpty() && s.isNotEmpty())
  444. trimmed = s.replaceCharacters ("\r\n\t", " ");
  445. return trimmed;
  446. }
  447. OwnedArray<Token> tokens;
  448. int totalLines = 0;
  449. JUCE_DECLARE_NON_COPYABLE (TokenList)
  450. };
  451. }
  452. //==============================================================================
  453. void TextLayout::createStandardLayout (const AttributedString& text)
  454. {
  455. TextLayoutHelpers::TokenList l;
  456. l.createLayout (text, *this);
  457. }
  458. void TextLayout::recalculateSize()
  459. {
  460. if (! lines.isEmpty())
  461. {
  462. auto bounds = lines.getFirst()->getLineBounds();
  463. for (auto* line : lines)
  464. bounds = bounds.getUnion (line->getLineBounds());
  465. for (auto* line : lines)
  466. line->lineOrigin.x -= bounds.getX();
  467. width = bounds.getWidth();
  468. height = bounds.getHeight();
  469. }
  470. else
  471. {
  472. width = 0;
  473. height = 0;
  474. }
  475. }
  476. } // namespace juce