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.

617 lines
18KB

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