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.

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, 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 (auto* run : runs)
  77. {
  78. for (auto& glyph : run->glyphs)
  79. {
  80. Range<float> runRange (glyph.anchor.x, glyph.anchor.x + glyph.width);
  81. if (isFirst)
  82. {
  83. isFirst = false;
  84. range = runRange;
  85. }
  86. else
  87. {
  88. range = range.getUnionWith (runRange);
  89. }
  90. }
  91. }
  92. return range + lineOrigin.x;
  93. }
  94. Range<float> TextLayout::Line::getLineBoundsY() const noexcept
  95. {
  96. return Range<float> (lineOrigin.y - ascent,
  97. lineOrigin.y + descent);
  98. }
  99. Rectangle<float> TextLayout::Line::getLineBounds() const noexcept
  100. {
  101. auto x = getLineBoundsX();
  102. auto y = getLineBoundsY();
  103. return { x.getStart(), y.getStart(), x.getLength(), y.getLength() };
  104. }
  105. //==============================================================================
  106. TextLayout::TextLayout()
  107. : width (0), height (0), justification (Justification::topLeft)
  108. {
  109. }
  110. TextLayout::TextLayout (const TextLayout& other)
  111. : width (other.width), height (other.height),
  112. justification (other.justification)
  113. {
  114. lines.addCopiesOf (other.lines);
  115. }
  116. TextLayout::TextLayout (TextLayout&& other) noexcept
  117. : lines (static_cast<OwnedArray<Line>&&> (other.lines)),
  118. width (other.width), height (other.height),
  119. justification (other.justification)
  120. {
  121. }
  122. TextLayout& TextLayout::operator= (TextLayout&& other) noexcept
  123. {
  124. lines = static_cast<OwnedArray<Line>&&> (other.lines);
  125. width = other.width;
  126. height = other.height;
  127. justification = other.justification;
  128. return *this;
  129. }
  130. TextLayout& TextLayout::operator= (const TextLayout& other)
  131. {
  132. width = other.width;
  133. height = other.height;
  134. justification = other.justification;
  135. lines.clear();
  136. lines.addCopiesOf (other.lines);
  137. return *this;
  138. }
  139. TextLayout::~TextLayout()
  140. {
  141. }
  142. TextLayout::Line& TextLayout::getLine (const int index) const
  143. {
  144. return *lines.getUnchecked (index);
  145. }
  146. void TextLayout::ensureStorageAllocated (int numLinesNeeded)
  147. {
  148. lines.ensureStorageAllocated (numLinesNeeded);
  149. }
  150. void TextLayout::addLine (Line* line)
  151. {
  152. lines.add (line);
  153. }
  154. void TextLayout::draw (Graphics& g, Rectangle<float> area) const
  155. {
  156. auto origin = justification.appliedToRectangle (Rectangle<float> (width, getHeight()), area).getPosition();
  157. auto& context = g.getInternalContext();
  158. for (auto* line : lines)
  159. {
  160. auto lineOrigin = origin + line->lineOrigin;
  161. for (auto* run : line->runs)
  162. {
  163. context.setFont (run->font);
  164. context.setFill (run->colour);
  165. for (auto& glyph : run->glyphs)
  166. context.drawGlyph (glyph.glyphCode, AffineTransform::translation (lineOrigin.x + glyph.anchor.x,
  167. lineOrigin.y + glyph.anchor.y));
  168. if (run->font.isUnderlined())
  169. {
  170. Range<float> runExtent;
  171. for (auto& glyph : run->glyphs)
  172. {
  173. Range<float> glyphRange (glyph.anchor.x, glyph.anchor.x + glyph.width);
  174. runExtent = runExtent.isEmpty() ? glyphRange
  175. : runExtent.getUnionWith (glyphRange);
  176. }
  177. const float lineThickness = run->font.getDescent() * 0.3f;
  178. context.fillRect ({ runExtent.getStart() + lineOrigin.x, lineOrigin.y + lineThickness * 2.0f,
  179. runExtent.getLength(), lineThickness });
  180. }
  181. }
  182. }
  183. }
  184. void TextLayout::createLayout (const AttributedString& text, float maxWidth)
  185. {
  186. createLayout (text, maxWidth, 1.0e7f);
  187. }
  188. void TextLayout::createLayout (const AttributedString& text, float maxWidth, float maxHeight)
  189. {
  190. lines.clear();
  191. width = maxWidth;
  192. height = maxHeight;
  193. justification = text.getJustification();
  194. if (! createNativeLayout (text))
  195. createStandardLayout (text);
  196. recalculateSize();
  197. }
  198. void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth)
  199. {
  200. createLayoutWithBalancedLineLengths (text, maxWidth, 1.0e7f);
  201. }
  202. void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& text, float maxWidth, float maxHeight)
  203. {
  204. const float minimumWidth = maxWidth / 2.0f;
  205. float bestWidth = maxWidth;
  206. float bestLineProportion = 0.0f;
  207. while (maxWidth > minimumWidth)
  208. {
  209. createLayout (text, maxWidth, maxHeight);
  210. if (getNumLines() < 2)
  211. return;
  212. const float line1 = lines.getUnchecked (lines.size() - 1)->getLineBoundsX().getLength();
  213. const float line2 = lines.getUnchecked (lines.size() - 2)->getLineBoundsX().getLength();
  214. const float shortestLine = jmin (line1, line2);
  215. const float prop = (shortestLine > 0) ? jmax (line1, line2) / shortestLine : 1.0f;
  216. if (prop > 0.9f)
  217. return;
  218. if (prop > bestLineProportion)
  219. {
  220. bestLineProportion = prop;
  221. bestWidth = maxWidth;
  222. }
  223. maxWidth -= 10.0f;
  224. }
  225. if (bestWidth != maxWidth)
  226. createLayout (text, bestWidth, maxHeight);
  227. }
  228. //==============================================================================
  229. namespace TextLayoutHelpers
  230. {
  231. struct Token
  232. {
  233. Token (const String& t, const Font& f, Colour c, bool whitespace)
  234. : text (t), font (f), colour (c),
  235. area (font.getStringWidthFloat (t), f.getHeight()),
  236. isWhitespace (whitespace),
  237. isNewLine (t.containsChar ('\n') || t.containsChar ('\r'))
  238. {}
  239. const String text;
  240. const Font font;
  241. const Colour colour;
  242. Rectangle<float> area;
  243. int line;
  244. float lineHeight;
  245. const bool isWhitespace, isNewLine;
  246. private:
  247. Token& operator= (const Token&);
  248. };
  249. struct TokenList
  250. {
  251. TokenList() noexcept {}
  252. void createLayout (const AttributedString& text, TextLayout& layout)
  253. {
  254. layout.ensureStorageAllocated (totalLines);
  255. addTextRuns (text);
  256. layoutRuns (layout.getWidth(), text.getLineSpacing(), text.getWordWrap());
  257. int charPosition = 0;
  258. int lineStartPosition = 0;
  259. int runStartPosition = 0;
  260. ScopedPointer<TextLayout::Line> currentLine;
  261. ScopedPointer<TextLayout::Run> currentRun;
  262. bool needToSetLineOrigin = true;
  263. for (int i = 0; i < tokens.size(); ++i)
  264. {
  265. auto& t = *tokens.getUnchecked (i);
  266. Array<int> newGlyphs;
  267. Array<float> xOffsets;
  268. t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets);
  269. if (currentRun == nullptr) currentRun = new TextLayout::Run();
  270. if (currentLine == nullptr) currentLine = new TextLayout::Line();
  271. if (newGlyphs.size() > 0)
  272. {
  273. currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size());
  274. const Point<float> tokenOrigin (t.area.getPosition().translated (0, t.font.getAscent()));
  275. if (needToSetLineOrigin)
  276. {
  277. needToSetLineOrigin = false;
  278. currentLine->lineOrigin = tokenOrigin;
  279. }
  280. const Point<float> glyphOffset (tokenOrigin - currentLine->lineOrigin);
  281. for (int j = 0; j < newGlyphs.size(); ++j)
  282. {
  283. const float x = xOffsets.getUnchecked (j);
  284. currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked(j),
  285. glyphOffset.translated (x, 0),
  286. xOffsets.getUnchecked (j + 1) - x));
  287. }
  288. charPosition += newGlyphs.size();
  289. }
  290. if (t.isWhitespace || t.isNewLine)
  291. ++charPosition;
  292. if (auto* nextToken = tokens [i + 1])
  293. {
  294. if (t.font != nextToken->font || t.colour != nextToken->colour)
  295. {
  296. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  297. runStartPosition = charPosition;
  298. }
  299. if (t.line != nextToken->line)
  300. {
  301. if (currentRun == nullptr)
  302. currentRun = new TextLayout::Run();
  303. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  304. currentLine->stringRange = Range<int> (lineStartPosition, charPosition);
  305. if (! needToSetLineOrigin)
  306. layout.addLine (currentLine.release());
  307. runStartPosition = charPosition;
  308. lineStartPosition = charPosition;
  309. needToSetLineOrigin = true;
  310. }
  311. }
  312. else
  313. {
  314. addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
  315. currentLine->stringRange = Range<int> (lineStartPosition, charPosition);
  316. if (! needToSetLineOrigin)
  317. layout.addLine (currentLine.release());
  318. needToSetLineOrigin = true;
  319. }
  320. }
  321. if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0)
  322. {
  323. const float totalW = layout.getWidth();
  324. const bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0;
  325. for (int i = 0; i < layout.getNumLines(); ++i)
  326. {
  327. float dx = totalW - layout.getLine(i).getLineBoundsX().getLength();
  328. if (isCentred)
  329. dx /= 2.0f;
  330. layout.getLine(i).lineOrigin.x += dx;
  331. }
  332. }
  333. }
  334. private:
  335. static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun,
  336. const Token& t, const int start, const int end)
  337. {
  338. glyphRun->stringRange = { start, end };
  339. glyphRun->font = t.font;
  340. glyphRun->colour = t.colour;
  341. glyphLine.ascent = jmax (glyphLine.ascent, t.font.getAscent());
  342. glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent());
  343. glyphLine.runs.add (glyphRun);
  344. }
  345. static int getCharacterType (const juce_wchar c) noexcept
  346. {
  347. if (c == '\r' || c == '\n')
  348. return 0;
  349. return CharacterFunctions::isWhitespace (c) ? 2 : 1;
  350. }
  351. void appendText (const String& stringText, const Font& font, Colour colour)
  352. {
  353. auto t = stringText.getCharPointer();
  354. String currentString;
  355. int lastCharType = 0;
  356. for (;;)
  357. {
  358. const juce_wchar c = t.getAndAdvance();
  359. if (c == 0)
  360. break;
  361. const int charType = getCharacterType (c);
  362. if (charType == 0 || charType != lastCharType)
  363. {
  364. if (currentString.isNotEmpty())
  365. tokens.add (new Token (currentString, font, colour,
  366. lastCharType == 2 || lastCharType == 0));
  367. currentString = String::charToString (c);
  368. if (c == '\r' && *t == '\n')
  369. currentString += t.getAndAdvance();
  370. }
  371. else
  372. {
  373. currentString += c;
  374. }
  375. lastCharType = charType;
  376. }
  377. if (currentString.isNotEmpty())
  378. tokens.add (new Token (currentString, font, colour, lastCharType == 2));
  379. }
  380. void layoutRuns (const float maxWidth, const float extraLineSpacing, const AttributedString::WordWrap wordWrap)
  381. {
  382. float x = 0, y = 0, h = 0;
  383. int i;
  384. for (i = 0; i < tokens.size(); ++i)
  385. {
  386. auto& t = *tokens.getUnchecked(i);
  387. t.area.setPosition (x, y);
  388. t.line = totalLines;
  389. x += t.area.getWidth();
  390. h = jmax (h, t.area.getHeight() + extraLineSpacing);
  391. auto* nextTok = tokens[i + 1];
  392. if (nextTok == nullptr)
  393. break;
  394. const bool tokenTooLarge = (x + nextTok->area.getWidth() > maxWidth);
  395. if (t.isNewLine || ((! nextTok->isWhitespace) && (tokenTooLarge && wordWrap != AttributedString::none)))
  396. {
  397. setLastLineHeight (i + 1, h);
  398. x = 0;
  399. y += h;
  400. h = 0;
  401. ++totalLines;
  402. }
  403. }
  404. setLastLineHeight (jmin (i + 1, tokens.size()), h);
  405. ++totalLines;
  406. }
  407. void setLastLineHeight (int i, const float height) noexcept
  408. {
  409. while (--i >= 0)
  410. {
  411. auto& tok = *tokens.getUnchecked (i);
  412. if (tok.line == totalLines)
  413. tok.lineHeight = height;
  414. else
  415. break;
  416. }
  417. }
  418. void addTextRuns (const AttributedString& text)
  419. {
  420. const int numAttributes = text.getNumAttributes();
  421. tokens.ensureStorageAllocated (jmax (64, numAttributes));
  422. for (int i = 0; i < numAttributes; ++i)
  423. {
  424. auto& attr = text.getAttribute (i);
  425. appendText (text.getText().substring (attr.range.getStart(), attr.range.getEnd()),
  426. attr.font, attr.colour);
  427. }
  428. }
  429. static String getTrimmedEndIfNotAllWhitespace (const String& s)
  430. {
  431. auto trimmed = s.trimEnd();
  432. if (trimmed.isEmpty() && s.isNotEmpty())
  433. trimmed = s.replaceCharacters ("\r\n\t", " ");
  434. return trimmed;
  435. }
  436. OwnedArray<Token> tokens;
  437. int totalLines = 0;
  438. JUCE_DECLARE_NON_COPYABLE (TokenList)
  439. };
  440. }
  441. //==============================================================================
  442. void TextLayout::createStandardLayout (const AttributedString& text)
  443. {
  444. TextLayoutHelpers::TokenList l;
  445. l.createLayout (text, *this);
  446. }
  447. void TextLayout::recalculateSize()
  448. {
  449. if (! lines.isEmpty())
  450. {
  451. auto bounds = lines.getFirst()->getLineBounds();
  452. for (auto* line : lines)
  453. bounds = bounds.getUnion (line->getLineBounds());
  454. for (auto* line : lines)
  455. line->lineOrigin.x -= bounds.getX();
  456. width = bounds.getWidth();
  457. height = bounds.getHeight();
  458. }
  459. else
  460. {
  461. width = 0;
  462. height = 0;
  463. }
  464. }