VST3: fetch midi mappings all at once, use it for note/sound-off
[carla.git] / source / modules / juce_graphics / fonts / juce_GlyphArrangement.cpp
blob648acfcc2591b0dcdb67699f650b81470edd8492
1 /*
2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
23 ==============================================================================
26 namespace juce
29 static constexpr bool isNonBreakingSpace (const juce_wchar c)
31 return c == 0x00a0
32 || c == 0x2007
33 || c == 0x202f
34 || c == 0x2060;
37 PositionedGlyph::PositionedGlyph() noexcept
38 : character (0), glyph (0), x (0), y (0), w (0), whitespace (false)
42 PositionedGlyph::PositionedGlyph (const Font& font_, juce_wchar character_, int glyphNumber,
43 float anchorX, float baselineY, float width, bool whitespace_)
44 : font (font_), character (character_), glyph (glyphNumber),
45 x (anchorX), y (baselineY), w (width), whitespace (whitespace_)
49 static void drawGlyphWithFont (Graphics& g, int glyph, const Font& font, AffineTransform t)
51 auto& context = g.getInternalContext();
52 context.setFont (font);
53 context.drawGlyph (glyph, t);
56 void PositionedGlyph::draw (Graphics& g) const
58 if (! isWhitespace())
59 drawGlyphWithFont (g, glyph, font, AffineTransform::translation (x, y));
62 void PositionedGlyph::draw (Graphics& g, AffineTransform transform) const
64 if (! isWhitespace())
65 drawGlyphWithFont (g, glyph, font, AffineTransform::translation (x, y).followedBy (transform));
68 void PositionedGlyph::createPath (Path& path) const
70 if (! isWhitespace())
72 if (auto t = font.getTypefacePtr())
74 Path p;
75 t->getOutlineForGlyph (glyph, p);
77 path.addPath (p, AffineTransform::scale (font.getHeight() * font.getHorizontalScale(), font.getHeight())
78 .translated (x, y));
83 bool PositionedGlyph::hitTest (float px, float py) const
85 if (getBounds().contains (px, py) && ! isWhitespace())
87 if (auto t = font.getTypefacePtr())
89 Path p;
90 t->getOutlineForGlyph (glyph, p);
92 AffineTransform::translation (-x, -y)
93 .scaled (1.0f / (font.getHeight() * font.getHorizontalScale()), 1.0f / font.getHeight())
94 .transformPoint (px, py);
96 return p.contains (px, py);
100 return false;
103 void PositionedGlyph::moveBy (float deltaX, float deltaY)
105 x += deltaX;
106 y += deltaY;
110 //==============================================================================
111 GlyphArrangement::GlyphArrangement()
113 glyphs.ensureStorageAllocated (128);
116 //==============================================================================
117 void GlyphArrangement::clear()
119 glyphs.clear();
122 PositionedGlyph& GlyphArrangement::getGlyph (int index) noexcept
124 return glyphs.getReference (index);
127 //==============================================================================
128 void GlyphArrangement::addGlyphArrangement (const GlyphArrangement& other)
130 glyphs.addArray (other.glyphs);
133 void GlyphArrangement::addGlyph (const PositionedGlyph& glyph)
135 glyphs.add (glyph);
138 void GlyphArrangement::removeRangeOfGlyphs (int startIndex, int num)
140 glyphs.removeRange (startIndex, num < 0 ? glyphs.size() : num);
143 //==============================================================================
144 void GlyphArrangement::addLineOfText (const Font& font, const String& text, float xOffset, float yOffset)
146 addCurtailedLineOfText (font, text, xOffset, yOffset, 1.0e10f, false);
149 void GlyphArrangement::addCurtailedLineOfText (const Font& font, const String& text,
150 float xOffset, float yOffset,
151 float maxWidthPixels, bool useEllipsis)
153 if (text.isNotEmpty())
155 Array<int> newGlyphs;
156 Array<float> xOffsets;
157 font.getGlyphPositions (text, newGlyphs, xOffsets);
158 auto textLen = newGlyphs.size();
159 glyphs.ensureStorageAllocated (glyphs.size() + textLen);
161 auto t = text.getCharPointer();
163 for (int i = 0; i < textLen; ++i)
165 auto nextX = xOffsets.getUnchecked (i + 1);
167 if (nextX > maxWidthPixels + 1.0f)
169 // curtail the string if it's too wide..
170 if (useEllipsis && textLen > 3 && glyphs.size() >= 3)
171 insertEllipsis (font, xOffset + maxWidthPixels, 0, glyphs.size());
173 break;
176 auto thisX = xOffsets.getUnchecked (i);
177 auto isWhitespace = isNonBreakingSpace (*t) || t.isWhitespace();
179 glyphs.add (PositionedGlyph (font, t.getAndAdvance(),
180 newGlyphs.getUnchecked(i),
181 xOffset + thisX, yOffset,
182 nextX - thisX, isWhitespace));
187 int GlyphArrangement::insertEllipsis (const Font& font, float maxXPos, int startIndex, int endIndex)
189 int numDeleted = 0;
191 if (! glyphs.isEmpty())
193 Array<int> dotGlyphs;
194 Array<float> dotXs;
195 font.getGlyphPositions ("..", dotGlyphs, dotXs);
197 auto dx = dotXs[1];
198 float xOffset = 0.0f, yOffset = 0.0f;
200 while (endIndex > startIndex)
202 auto& pg = glyphs.getReference (--endIndex);
203 xOffset = pg.x;
204 yOffset = pg.y;
206 glyphs.remove (endIndex);
207 ++numDeleted;
209 if (xOffset + dx * 3 <= maxXPos)
210 break;
213 for (int i = 3; --i >= 0;)
215 glyphs.insert (endIndex++, PositionedGlyph (font, '.', dotGlyphs.getFirst(),
216 xOffset, yOffset, dx, false));
217 --numDeleted;
218 xOffset += dx;
220 if (xOffset > maxXPos)
221 break;
225 return numDeleted;
228 void GlyphArrangement::addJustifiedText (const Font& font, const String& text,
229 float x, float y, float maxLineWidth,
230 Justification horizontalLayout,
231 float leading)
233 auto lineStartIndex = glyphs.size();
234 addLineOfText (font, text, x, y);
236 auto originalY = y;
238 while (lineStartIndex < glyphs.size())
240 int i = lineStartIndex;
242 if (glyphs.getReference(i).getCharacter() != '\n'
243 && glyphs.getReference(i).getCharacter() != '\r')
244 ++i;
246 auto lineMaxX = glyphs.getReference (lineStartIndex).getLeft() + maxLineWidth;
247 int lastWordBreakIndex = -1;
249 while (i < glyphs.size())
251 auto& pg = glyphs.getReference (i);
252 auto c = pg.getCharacter();
254 if (c == '\r' || c == '\n')
256 ++i;
258 if (c == '\r' && i < glyphs.size()
259 && glyphs.getReference(i).getCharacter() == '\n')
260 ++i;
262 break;
265 if (pg.isWhitespace())
267 lastWordBreakIndex = i + 1;
269 else if (pg.getRight() - 0.0001f >= lineMaxX)
271 if (lastWordBreakIndex >= 0)
272 i = lastWordBreakIndex;
274 break;
277 ++i;
280 auto currentLineStartX = glyphs.getReference (lineStartIndex).getLeft();
281 auto currentLineEndX = currentLineStartX;
283 for (int j = i; --j >= lineStartIndex;)
285 if (! glyphs.getReference (j).isWhitespace())
287 currentLineEndX = glyphs.getReference (j).getRight();
288 break;
292 float deltaX = 0.0f;
294 if (horizontalLayout.testFlags (Justification::horizontallyJustified))
295 spreadOutLine (lineStartIndex, i - lineStartIndex, maxLineWidth);
296 else if (horizontalLayout.testFlags (Justification::horizontallyCentred))
297 deltaX = (maxLineWidth - (currentLineEndX - currentLineStartX)) * 0.5f;
298 else if (horizontalLayout.testFlags (Justification::right))
299 deltaX = maxLineWidth - (currentLineEndX - currentLineStartX);
301 moveRangeOfGlyphs (lineStartIndex, i - lineStartIndex,
302 x + deltaX - currentLineStartX, y - originalY);
304 lineStartIndex = i;
306 y += font.getHeight() + leading;
310 void GlyphArrangement::addFittedText (const Font& f, const String& text,
311 float x, float y, float width, float height,
312 Justification layout, int maximumLines,
313 float minimumHorizontalScale)
315 if (minimumHorizontalScale == 0.0f)
316 minimumHorizontalScale = Font::getDefaultMinimumHorizontalScaleFactor();
318 // doesn't make much sense if this is outside a sensible range of 0.5 to 1.0
319 jassert (minimumHorizontalScale > 0 && minimumHorizontalScale <= 1.0f);
321 if (text.containsAnyOf ("\r\n"))
323 addLinesWithLineBreaks (text, f, x, y, width, height, layout);
325 else
327 auto startIndex = glyphs.size();
328 auto trimmed = text.trim();
329 addLineOfText (f, trimmed, x, y);
330 auto numGlyphs = glyphs.size() - startIndex;
332 if (numGlyphs > 0)
334 auto lineWidth = glyphs.getReference (glyphs.size() - 1).getRight()
335 - glyphs.getReference (startIndex).getLeft();
337 if (lineWidth > 0)
339 if (lineWidth * minimumHorizontalScale < width)
341 if (lineWidth > width)
342 stretchRangeOfGlyphs (startIndex, numGlyphs, width / lineWidth);
344 justifyGlyphs (startIndex, numGlyphs, x, y, width, height, layout);
346 else if (maximumLines <= 1)
348 fitLineIntoSpace (startIndex, numGlyphs, x, y, width, height,
349 f, layout, minimumHorizontalScale);
351 else
353 splitLines (trimmed, f, startIndex, x, y, width, height,
354 maximumLines, lineWidth, layout, minimumHorizontalScale);
361 //==============================================================================
362 void GlyphArrangement::moveRangeOfGlyphs (int startIndex, int num, const float dx, const float dy)
364 jassert (startIndex >= 0);
366 if (dx != 0.0f || dy != 0.0f)
368 if (num < 0 || startIndex + num > glyphs.size())
369 num = glyphs.size() - startIndex;
371 while (--num >= 0)
372 glyphs.getReference (startIndex++).moveBy (dx, dy);
376 void GlyphArrangement::addLinesWithLineBreaks (const String& text, const Font& f,
377 float x, float y, float width, float height, Justification layout)
379 GlyphArrangement ga;
380 ga.addJustifiedText (f, text, x, y, width, layout);
382 auto bb = ga.getBoundingBox (0, -1, false);
383 auto dy = y - bb.getY();
385 if (layout.testFlags (Justification::verticallyCentred)) dy += (height - bb.getHeight()) * 0.5f;
386 else if (layout.testFlags (Justification::bottom)) dy += (height - bb.getHeight());
388 ga.moveRangeOfGlyphs (0, -1, 0.0f, dy);
390 glyphs.addArray (ga.glyphs);
393 int GlyphArrangement::fitLineIntoSpace (int start, int numGlyphs, float x, float y, float w, float h, const Font& font,
394 Justification justification, float minimumHorizontalScale)
396 int numDeleted = 0;
397 auto lineStartX = glyphs.getReference (start).getLeft();
398 auto lineWidth = glyphs.getReference (start + numGlyphs - 1).getRight() - lineStartX;
400 if (lineWidth > w)
402 if (minimumHorizontalScale < 1.0f)
404 stretchRangeOfGlyphs (start, numGlyphs, jmax (minimumHorizontalScale, w / lineWidth));
405 lineWidth = glyphs.getReference (start + numGlyphs - 1).getRight() - lineStartX - 0.5f;
408 if (lineWidth > w)
410 numDeleted = insertEllipsis (font, lineStartX + w, start, start + numGlyphs);
411 numGlyphs -= numDeleted;
415 justifyGlyphs (start, numGlyphs, x, y, w, h, justification);
416 return numDeleted;
419 void GlyphArrangement::stretchRangeOfGlyphs (int startIndex, int num, float horizontalScaleFactor)
421 jassert (startIndex >= 0);
423 if (num < 0 || startIndex + num > glyphs.size())
424 num = glyphs.size() - startIndex;
426 if (num > 0)
428 auto xAnchor = glyphs.getReference (startIndex).getLeft();
430 while (--num >= 0)
432 auto& pg = glyphs.getReference (startIndex++);
434 pg.x = xAnchor + (pg.x - xAnchor) * horizontalScaleFactor;
435 pg.font.setHorizontalScale (pg.font.getHorizontalScale() * horizontalScaleFactor);
436 pg.w *= horizontalScaleFactor;
441 Rectangle<float> GlyphArrangement::getBoundingBox (int startIndex, int num, bool includeWhitespace) const
443 jassert (startIndex >= 0);
445 if (num < 0 || startIndex + num > glyphs.size())
446 num = glyphs.size() - startIndex;
448 Rectangle<float> result;
450 while (--num >= 0)
452 auto& pg = glyphs.getReference (startIndex++);
454 if (includeWhitespace || ! pg.isWhitespace())
455 result = result.getUnion (pg.getBounds());
458 return result;
461 void GlyphArrangement::justifyGlyphs (int startIndex, int num,
462 float x, float y, float width, float height,
463 Justification justification)
465 jassert (num >= 0 && startIndex >= 0);
467 if (glyphs.size() > 0 && num > 0)
469 auto bb = getBoundingBox (startIndex, num, ! justification.testFlags (Justification::horizontallyJustified
470 | Justification::horizontallyCentred));
471 float deltaX = x, deltaY = y;
473 if (justification.testFlags (Justification::horizontallyJustified)) deltaX -= bb.getX();
474 else if (justification.testFlags (Justification::horizontallyCentred)) deltaX += (width - bb.getWidth()) * 0.5f - bb.getX();
475 else if (justification.testFlags (Justification::right)) deltaX += width - bb.getRight();
476 else deltaX -= bb.getX();
478 if (justification.testFlags (Justification::top)) deltaY -= bb.getY();
479 else if (justification.testFlags (Justification::bottom)) deltaY += height - bb.getBottom();
480 else deltaY += (height - bb.getHeight()) * 0.5f - bb.getY();
482 moveRangeOfGlyphs (startIndex, num, deltaX, deltaY);
484 if (justification.testFlags (Justification::horizontallyJustified))
486 int lineStart = 0;
487 auto baseY = glyphs.getReference (startIndex).getBaselineY();
489 int i;
490 for (i = 0; i < num; ++i)
492 auto glyphY = glyphs.getReference (startIndex + i).getBaselineY();
494 if (glyphY != baseY)
496 spreadOutLine (startIndex + lineStart, i - lineStart, width);
498 lineStart = i;
499 baseY = glyphY;
503 if (i > lineStart)
504 spreadOutLine (startIndex + lineStart, i - lineStart, width);
509 void GlyphArrangement::spreadOutLine (int start, int num, float targetWidth)
511 if (start + num < glyphs.size()
512 && glyphs.getReference (start + num - 1).getCharacter() != '\r'
513 && glyphs.getReference (start + num - 1).getCharacter() != '\n')
515 int numSpaces = 0;
516 int spacesAtEnd = 0;
518 for (int i = 0; i < num; ++i)
520 if (glyphs.getReference (start + i).isWhitespace())
522 ++spacesAtEnd;
523 ++numSpaces;
525 else
527 spacesAtEnd = 0;
531 numSpaces -= spacesAtEnd;
533 if (numSpaces > 0)
535 auto startX = glyphs.getReference (start).getLeft();
536 auto endX = glyphs.getReference (start + num - 1 - spacesAtEnd).getRight();
538 auto extraPaddingBetweenWords = (targetWidth - (endX - startX)) / (float) numSpaces;
539 float deltaX = 0.0f;
541 for (int i = 0; i < num; ++i)
543 glyphs.getReference (start + i).moveBy (deltaX, 0.0f);
545 if (glyphs.getReference (start + i).isWhitespace())
546 deltaX += extraPaddingBetweenWords;
552 static bool isBreakableGlyph (const PositionedGlyph& g) noexcept
554 return ! isNonBreakingSpace (g.getCharacter()) && (g.isWhitespace() || g.getCharacter() == '-');
557 void GlyphArrangement::splitLines (const String& text, Font font, int startIndex,
558 float x, float y, float width, float height, int maximumLines,
559 float lineWidth, Justification layout, float minimumHorizontalScale)
561 auto length = text.length();
562 auto originalStartIndex = startIndex;
563 int numLines = 1;
565 if (length <= 12 && ! text.containsAnyOf (" -\t\r\n"))
566 maximumLines = 1;
568 maximumLines = jmin (maximumLines, length);
570 while (numLines < maximumLines)
572 ++numLines;
573 auto newFontHeight = height / (float) numLines;
575 if (newFontHeight < font.getHeight())
577 font.setHeight (jmax (8.0f, newFontHeight));
579 removeRangeOfGlyphs (startIndex, -1);
580 addLineOfText (font, text, x, y);
582 lineWidth = glyphs.getReference (glyphs.size() - 1).getRight()
583 - glyphs.getReference (startIndex).getLeft();
586 // Try to estimate the point at which there are enough lines to fit the text,
587 // allowing for unevenness in the lengths due to differently sized words.
588 const float lineLengthUnevennessAllowance = 80.0f;
590 if ((float) numLines > (lineWidth + lineLengthUnevennessAllowance) / width || newFontHeight < 8.0f)
591 break;
594 if (numLines < 1)
595 numLines = 1;
597 int lineIndex = 0;
598 auto lineY = y;
599 auto widthPerLine = jmin (width / minimumHorizontalScale,
600 lineWidth / (float) numLines);
602 while (lineY < y + height)
604 auto endIndex = startIndex;
605 auto lineStartX = glyphs.getReference (startIndex).getLeft();
606 auto lineBottomY = lineY + font.getHeight();
608 if (lineIndex++ >= numLines - 1
609 || lineBottomY >= y + height)
611 widthPerLine = width;
612 endIndex = glyphs.size();
614 else
616 while (endIndex < glyphs.size())
618 if (glyphs.getReference (endIndex).getRight() - lineStartX > widthPerLine)
620 // got to a point where the line's too long, so skip forward to find a
621 // good place to break it..
622 auto searchStartIndex = endIndex;
624 while (endIndex < glyphs.size())
626 auto& g = glyphs.getReference (endIndex);
628 if ((g.getRight() - lineStartX) * minimumHorizontalScale < width)
630 if (isBreakableGlyph (g))
632 ++endIndex;
633 break;
636 else
638 // can't find a suitable break, so try looking backwards..
639 endIndex = searchStartIndex;
641 for (int back = 1; back < jmin (7, endIndex - startIndex - 1); ++back)
643 if (isBreakableGlyph (glyphs.getReference (endIndex - back)))
645 endIndex -= back - 1;
646 break;
650 break;
653 ++endIndex;
656 break;
659 ++endIndex;
662 auto wsStart = endIndex;
663 auto wsEnd = endIndex;
665 while (wsStart > 0 && glyphs.getReference (wsStart - 1).isWhitespace())
666 --wsStart;
668 while (wsEnd < glyphs.size() && glyphs.getReference (wsEnd).isWhitespace())
669 ++wsEnd;
671 removeRangeOfGlyphs (wsStart, wsEnd - wsStart);
672 endIndex = jmax (wsStart, startIndex + 1);
675 endIndex -= fitLineIntoSpace (startIndex, endIndex - startIndex,
676 x, lineY, width, font.getHeight(), font,
677 layout.getOnlyHorizontalFlags() | Justification::verticallyCentred,
678 minimumHorizontalScale);
680 startIndex = endIndex;
681 lineY = lineBottomY;
683 if (startIndex >= glyphs.size())
684 break;
687 justifyGlyphs (originalStartIndex, glyphs.size() - originalStartIndex,
688 x, y, width, height, layout.getFlags() & ~Justification::horizontallyJustified);
691 //==============================================================================
692 void GlyphArrangement::drawGlyphUnderline (const Graphics& g, const PositionedGlyph& pg,
693 int i, AffineTransform transform) const
695 auto lineThickness = (pg.font.getDescent()) * 0.3f;
696 auto nextX = pg.x + pg.w;
698 if (i < glyphs.size() - 1 && glyphs.getReference (i + 1).y == pg.y)
699 nextX = glyphs.getReference (i + 1).x;
701 Path p;
702 p.addRectangle (pg.x, pg.y + lineThickness * 2.0f, nextX - pg.x, lineThickness);
703 g.fillPath (p, transform);
706 void GlyphArrangement::draw (const Graphics& g) const
708 draw (g, {});
711 void GlyphArrangement::draw (const Graphics& g, AffineTransform transform) const
713 auto& context = g.getInternalContext();
714 auto lastFont = context.getFont();
715 bool needToRestore = false;
717 for (int i = 0; i < glyphs.size(); ++i)
719 auto& pg = glyphs.getReference (i);
721 if (pg.font.isUnderlined())
722 drawGlyphUnderline (g, pg, i, transform);
724 if (! pg.isWhitespace())
726 if (lastFont != pg.font)
728 lastFont = pg.font;
730 if (! needToRestore)
732 needToRestore = true;
733 context.saveState();
736 context.setFont (lastFont);
739 context.drawGlyph (pg.glyph, AffineTransform::translation (pg.x, pg.y)
740 .followedBy (transform));
744 if (needToRestore)
745 context.restoreState();
748 void GlyphArrangement::createPath (Path& path) const
750 for (auto& g : glyphs)
751 g.createPath (path);
754 int GlyphArrangement::findGlyphIndexAt (float x, float y) const
756 for (int i = 0; i < glyphs.size(); ++i)
757 if (glyphs.getReference (i).hitTest (x, y))
758 return i;
760 return -1;
763 } // namespace juce