The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
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.

621 lines
19KB

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