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.

584 lines
18KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2015 - ROLI 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();
  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 Token
  235. {
  236. Token (const String& t, const Font& f, Colour c, const bool whitespace)
  237. : text (t), font (f), colour (c),
  238. area (font.getStringWidthFloat (t), f.getHeight()),
  239. isWhitespace (whitespace),
  240. isNewLine (t.containsChar ('\n') || t.containsChar ('\r'))
  241. {}
  242. const String text;
  243. const Font font;
  244. const Colour colour;
  245. Rectangle<float> area;
  246. int line;
  247. float lineHeight;
  248. const bool isWhitespace, isNewLine;
  249. private:
  250. Token& operator= (const Token&);
  251. };
  252. struct TokenList
  253. {
  254. TokenList() noexcept : totalLines (0) {}
  255. void createLayout (const AttributedString& text, TextLayout& layout)
  256. {
  257. layout.ensureStorageAllocated (totalLines);
  258. addTextRuns (text);
  259. layoutRuns (layout.getWidth(), text.getLineSpacing());
  260. int charPosition = 0;
  261. int lineStartPosition = 0;
  262. int runStartPosition = 0;
  263. ScopedPointer<TextLayout::Line> currentLine;
  264. ScopedPointer<TextLayout::Run> currentRun;
  265. bool needToSetLineOrigin = true;
  266. for (int i = 0; i < tokens.size(); ++i)
  267. {
  268. const Token& t = *tokens.getUnchecked (i);
  269. Array<int> newGlyphs;
  270. Array<float> xOffsets;
  271. t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets);
  272. if (currentRun == nullptr) currentRun = new TextLayout::Run();
  273. if (currentLine == nullptr) currentLine = new TextLayout::Line();
  274. if (newGlyphs.size() > 0)
  275. {
  276. currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size());
  277. const Point<float> tokenOrigin (t.area.getPosition().translated (0, t.font.getAscent()));
  278. if (needToSetLineOrigin)
  279. {
  280. needToSetLineOrigin = false;
  281. currentLine->lineOrigin = tokenOrigin;
  282. }
  283. const Point<float> glyphOffset (tokenOrigin - currentLine->lineOrigin);
  284. for (int j = 0; j < newGlyphs.size(); ++j)
  285. {
  286. const float x = xOffsets.getUnchecked (j);
  287. currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked(j),
  288. glyphOffset.translated (x, 0),
  289. xOffsets.getUnchecked (j + 1) - x));
  290. }
  291. charPosition += newGlyphs.size();
  292. }
  293. if (t.isWhitespace || t.isNewLine)
  294. ++charPosition;
  295. const Token* const nextToken = tokens [i + 1];
  296. if (nextToken == nullptr) // this is the last token
  297. {
  298. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  299. currentLine->stringRange = Range<int> (lineStartPosition, charPosition);
  300. if (! needToSetLineOrigin)
  301. layout.addLine (currentLine.release());
  302. needToSetLineOrigin = true;
  303. }
  304. else
  305. {
  306. if (t.font != nextToken->font || t.colour != nextToken->colour)
  307. {
  308. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  309. runStartPosition = charPosition;
  310. }
  311. if (t.line != nextToken->line)
  312. {
  313. if (currentRun == nullptr)
  314. currentRun = new TextLayout::Run();
  315. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  316. currentLine->stringRange = Range<int> (lineStartPosition, charPosition);
  317. if (! needToSetLineOrigin)
  318. layout.addLine (currentLine.release());
  319. runStartPosition = charPosition;
  320. lineStartPosition = charPosition;
  321. needToSetLineOrigin = true;
  322. }
  323. }
  324. }
  325. if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0)
  326. {
  327. const float totalW = layout.getWidth();
  328. const bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0;
  329. for (int i = 0; i < layout.getNumLines(); ++i)
  330. {
  331. float dx = totalW - layout.getLine(i).getLineBoundsX().getLength();
  332. if (isCentred)
  333. dx /= 2.0f;
  334. layout.getLine(i).lineOrigin.x += dx;
  335. }
  336. }
  337. }
  338. private:
  339. static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun,
  340. const Token& t, const int start, const int end)
  341. {
  342. glyphRun->stringRange = Range<int> (start, end);
  343. glyphRun->font = t.font;
  344. glyphRun->colour = t.colour;
  345. glyphLine.ascent = jmax (glyphLine.ascent, t.font.getAscent());
  346. glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent());
  347. glyphLine.runs.add (glyphRun);
  348. }
  349. static int getCharacterType (const juce_wchar c) noexcept
  350. {
  351. if (c == '\r' || c == '\n')
  352. return 0;
  353. return CharacterFunctions::isWhitespace (c) ? 2 : 1;
  354. }
  355. void appendText (const String& stringText, const Font& font, Colour colour)
  356. {
  357. String::CharPointerType t (stringText.getCharPointer());
  358. String currentString;
  359. int lastCharType = 0;
  360. for (;;)
  361. {
  362. const juce_wchar c = t.getAndAdvance();
  363. if (c == 0)
  364. break;
  365. const int charType = getCharacterType (c);
  366. if (charType == 0 || charType != lastCharType)
  367. {
  368. if (currentString.isNotEmpty())
  369. tokens.add (new Token (currentString, font, colour,
  370. lastCharType == 2 || lastCharType == 0));
  371. currentString = String::charToString (c);
  372. if (c == '\r' && *t == '\n')
  373. currentString += t.getAndAdvance();
  374. }
  375. else
  376. {
  377. currentString += c;
  378. }
  379. lastCharType = charType;
  380. }
  381. if (currentString.isNotEmpty())
  382. tokens.add (new Token (currentString, font, colour, lastCharType == 2));
  383. }
  384. void layoutRuns (const float maxWidth, const float extraLineSpacing)
  385. {
  386. float x = 0, y = 0, h = 0;
  387. int i;
  388. for (i = 0; i < tokens.size(); ++i)
  389. {
  390. Token& t = *tokens.getUnchecked(i);
  391. t.area.setPosition (x, y);
  392. t.line = totalLines;
  393. x += t.area.getWidth();
  394. h = jmax (h, t.area.getHeight() + extraLineSpacing);
  395. const Token* const nextTok = tokens[i + 1];
  396. if (nextTok == nullptr)
  397. break;
  398. if (t.isNewLine || ((! nextTok->isWhitespace) && x + nextTok->area.getWidth() > maxWidth))
  399. {
  400. setLastLineHeight (i + 1, h);
  401. x = 0;
  402. y += h;
  403. h = 0;
  404. ++totalLines;
  405. }
  406. }
  407. setLastLineHeight (jmin (i + 1, tokens.size()), h);
  408. ++totalLines;
  409. }
  410. void setLastLineHeight (int i, const float height) noexcept
  411. {
  412. while (--i >= 0)
  413. {
  414. Token& tok = *tokens.getUnchecked (i);
  415. if (tok.line == totalLines)
  416. tok.lineHeight = height;
  417. else
  418. break;
  419. }
  420. }
  421. void addTextRuns (const AttributedString& text)
  422. {
  423. const int numAttributes = text.getNumAttributes();
  424. tokens.ensureStorageAllocated (jmax (64, numAttributes));
  425. for (int i = 0; i < numAttributes; ++i)
  426. {
  427. const AttributedString::Attribute& attr = text.getAttribute (i);
  428. appendText (text.getText().substring (attr.range.getStart(), attr.range.getEnd()),
  429. attr.font, attr.colour);
  430. }
  431. }
  432. static String getTrimmedEndIfNotAllWhitespace (const String& s)
  433. {
  434. String trimmed (s.trimEnd());
  435. if (trimmed.isEmpty() && s.isNotEmpty())
  436. trimmed = s.replaceCharacters ("\r\n\t", " ");
  437. return trimmed;
  438. }
  439. OwnedArray<Token> tokens;
  440. int totalLines;
  441. JUCE_DECLARE_NON_COPYABLE (TokenList)
  442. };
  443. }
  444. //==============================================================================
  445. void TextLayout::createStandardLayout (const AttributedString& text)
  446. {
  447. TextLayoutHelpers::TokenList l;
  448. l.createLayout (text, *this);
  449. }
  450. void TextLayout::recalculateSize()
  451. {
  452. if (lines.size() > 0)
  453. {
  454. Rectangle<float> bounds (lines.getFirst()->getLineBounds());
  455. for (int i = lines.size(); --i > 0;)
  456. bounds = bounds.getUnion (lines.getUnchecked(i)->getLineBounds());
  457. for (int i = lines.size(); --i >= 0;)
  458. lines.getUnchecked(i)->lineOrigin.x -= bounds.getX();
  459. width = bounds.getWidth();
  460. height = bounds.getHeight();
  461. }
  462. else
  463. {
  464. width = 0;
  465. height = 0;
  466. }
  467. }