/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct GraphicsFontHelpers { static auto compareFont (const Font& a, const Font& b) { return Font::compare (a, b); } }; static auto operator< (const Font& a, const Font& b) { return GraphicsFontHelpers::compareFont (a, b); } template static auto operator< (const Rectangle& a, const Rectangle& b) { const auto tie = [] (auto& t) { return std::make_tuple (t.getX(), t.getY(), t.getWidth(), t.getHeight()); }; return tie (a) < tie (b); } static auto operator< (const Justification& a, const Justification& b) { return a.getFlags() < b.getFlags(); } //============================================================================== namespace { struct ConfiguredArrangement { void draw (const Graphics& g) const { arrangement.draw (g, transform); } GlyphArrangement arrangement; AffineTransform transform; }; template class GlyphArrangementCache final : public DeletedAtShutdown { public: GlyphArrangementCache() = default; ~GlyphArrangementCache() override { clearSingletonInstance(); } template void draw (const Graphics& g, ArrangementArgs&& args, ConfigureArrangement&& configureArrangement) { const ScopedTryLock stl (lock); if (! stl.isLocked()) { configureArrangement (args).draw (g); return; } const auto cached = [&] { const auto iter = cache.find (args); if (iter != cache.end()) { if (iter->second.cachePosition != cacheOrder.begin()) cacheOrder.splice (cacheOrder.begin(), cacheOrder, iter->second.cachePosition); return iter; } auto result = cache.emplace (std::move (args), CachedGlyphArrangement { configureArrangement (args), {} }).first; cacheOrder.push_front (result); return result; }(); cached->second.cachePosition = cacheOrder.begin(); cached->second.configured.draw (g); while (cache.size() > cacheSize) { cache.erase (cacheOrder.back()); cacheOrder.pop_back(); } } JUCE_DECLARE_SINGLETON (GlyphArrangementCache, false) private: struct CachedGlyphArrangement { using CachePtr = typename std::map::const_iterator; ConfiguredArrangement configured; typename std::list::const_iterator cachePosition; }; static constexpr size_t cacheSize = 128; std::map cache; std::list cacheOrder; CriticalSection lock; }; template juce::SingletonHolder, juce::CriticalSection, false> GlyphArrangementCache::singletonHolder; //============================================================================== template Rectangle coordsToRectangle (Type x, Type y, Type w, Type h) noexcept { #if JUCE_DEBUG const int maxVal = 0x3fffffff; jassertquiet ((int) x >= -maxVal && (int) x <= maxVal && (int) y >= -maxVal && (int) y <= maxVal && (int) w >= 0 && (int) w <= maxVal && (int) h >= 0 && (int) h <= maxVal); #endif return { x, y, w, h }; } } //============================================================================== Graphics::Graphics (const Image& imageToDrawOnto) : contextHolder (imageToDrawOnto.createLowLevelContext()), context (*contextHolder) { jassert (imageToDrawOnto.isValid()); // Can't draw into a null image! } Graphics::Graphics (LowLevelGraphicsContext& internalContext) noexcept : context (internalContext) { } //============================================================================== void Graphics::resetToDefaultState() { saveStateIfPending(); context.setFill (FillType()); context.setFont (Font()); context.setInterpolationQuality (Graphics::mediumResamplingQuality); } bool Graphics::isVectorDevice() const { return context.isVectorDevice(); } bool Graphics::reduceClipRegion (Rectangle area) { saveStateIfPending(); return context.clipToRectangle (area); } bool Graphics::reduceClipRegion (int x, int y, int w, int h) { return reduceClipRegion (coordsToRectangle (x, y, w, h)); } bool Graphics::reduceClipRegion (const RectangleList& clipRegion) { saveStateIfPending(); return context.clipToRectangleList (clipRegion); } bool Graphics::reduceClipRegion (const Path& path, const AffineTransform& transform) { saveStateIfPending(); context.clipToPath (path, transform); return ! context.isClipEmpty(); } bool Graphics::reduceClipRegion (const Image& image, const AffineTransform& transform) { saveStateIfPending(); context.clipToImageAlpha (image, transform); return ! context.isClipEmpty(); } void Graphics::excludeClipRegion (Rectangle rectangleToExclude) { saveStateIfPending(); context.excludeClipRectangle (rectangleToExclude); } bool Graphics::isClipEmpty() const { return context.isClipEmpty(); } Rectangle Graphics::getClipBounds() const { return context.getClipBounds(); } void Graphics::saveState() { saveStateIfPending(); saveStatePending = true; } void Graphics::restoreState() { if (saveStatePending) saveStatePending = false; else context.restoreState(); } void Graphics::saveStateIfPending() { if (saveStatePending) { saveStatePending = false; context.saveState(); } } void Graphics::setOrigin (Point newOrigin) { saveStateIfPending(); context.setOrigin (newOrigin); } void Graphics::setOrigin (int x, int y) { setOrigin ({ x, y }); } void Graphics::addTransform (const AffineTransform& transform) { saveStateIfPending(); context.addTransform (transform); } bool Graphics::clipRegionIntersects (Rectangle area) const { return context.clipRegionIntersects (area); } void Graphics::beginTransparencyLayer (float layerOpacity) { saveStateIfPending(); context.beginTransparencyLayer (layerOpacity); } void Graphics::endTransparencyLayer() { context.endTransparencyLayer(); } //============================================================================== void Graphics::setColour (Colour newColour) { saveStateIfPending(); context.setFill (newColour); } void Graphics::setOpacity (float newOpacity) { saveStateIfPending(); context.setOpacity (newOpacity); } void Graphics::setGradientFill (const ColourGradient& gradient) { setFillType (gradient); } void Graphics::setGradientFill (ColourGradient&& gradient) { setFillType (std::move (gradient)); } void Graphics::setTiledImageFill (const Image& imageToUse, const int anchorX, const int anchorY, const float opacity) { saveStateIfPending(); context.setFill (FillType (imageToUse, AffineTransform::translation ((float) anchorX, (float) anchorY))); context.setOpacity (opacity); } void Graphics::setFillType (const FillType& newFill) { saveStateIfPending(); context.setFill (newFill); } //============================================================================== void Graphics::setFont (const Font& newFont) { saveStateIfPending(); context.setFont (newFont); } void Graphics::setFont (const float newFontHeight) { setFont (context.getFont().withHeight (newFontHeight)); } Font Graphics::getCurrentFont() const { return context.getFont(); } //============================================================================== void Graphics::drawSingleLineText (const String& text, const int startX, const int baselineY, Justification justification) const { if (text.isEmpty()) return; // Don't pass any vertical placement flags to this method - they'll be ignored. jassert (justification.getOnlyVerticalFlags() == 0); auto flags = justification.getOnlyHorizontalFlags(); if (flags == Justification::right && startX < context.getClipBounds().getX()) return; if (flags == Justification::left && startX > context.getClipBounds().getRight()) return; struct ArrangementArgs { auto tie() const noexcept { return std::tie (font, text, startX, baselineY); } bool operator< (const ArrangementArgs& other) const { return tie() < other.tie(); } const Font font; const String text; const int startX, baselineY, flags; }; auto configureArrangement = [] (const ArrangementArgs& args) { AffineTransform transform; GlyphArrangement arrangement; arrangement.addLineOfText (args.font, args.text, (float) args.startX, (float) args.baselineY); if (args.flags != Justification::left) { auto w = arrangement.getBoundingBox (0, -1, true).getWidth(); if ((args.flags & (Justification::horizontallyCentred | Justification::horizontallyJustified)) != 0) w /= 2.0f; transform = AffineTransform::translation (-w, 0); } return ConfiguredArrangement { std::move (arrangement), std::move (transform) }; }; GlyphArrangementCache::getInstance()->draw (*this, { context.getFont(), text, startX, baselineY, flags }, std::move (configureArrangement)); } void Graphics::drawMultiLineText (const String& text, const int startX, const int baselineY, const int maximumLineWidth, Justification justification, const float leading) const { if (text.isEmpty() || startX >= context.getClipBounds().getRight()) return; struct ArrangementArgs { auto tie() const noexcept { return std::tie (font, text, startX, baselineY, maximumLineWidth, justification, leading); } bool operator< (const ArrangementArgs& other) const { return tie() < other.tie(); } const Font font; const String text; const int startX, baselineY, maximumLineWidth; const Justification justification; const float leading; }; auto configureArrangement = [] (const ArrangementArgs& args) { GlyphArrangement arrangement; arrangement.addJustifiedText (args.font, args.text, (float) args.startX, (float) args.baselineY, (float) args.maximumLineWidth, args.justification, args.leading); return ConfiguredArrangement { std::move (arrangement), {} }; }; GlyphArrangementCache::getInstance()->draw (*this, { context.getFont(), text, startX, baselineY, maximumLineWidth, justification, leading }, std::move (configureArrangement)); } void Graphics::drawText (const String& text, Rectangle area, Justification justificationType, bool useEllipsesIfTooBig) const { if (text.isEmpty() || ! context.clipRegionIntersects (area.getSmallestIntegerContainer())) return; struct ArrangementArgs { auto tie() const noexcept { return std::tie (font, text, area, justificationType, useEllipsesIfTooBig); } bool operator< (const ArrangementArgs& other) const { return tie() < other.tie(); } const Font font; const String text; const Rectangle area; const Justification justificationType; const bool useEllipsesIfTooBig; }; auto configureArrangement = [] (const ArrangementArgs& args) { GlyphArrangement arrangement; arrangement.addCurtailedLineOfText (args.font, args.text, 0.0f, 0.0f, args.area.getWidth(), args.useEllipsesIfTooBig); arrangement.justifyGlyphs (0, arrangement.getNumGlyphs(), args.area.getX(), args.area.getY(), args.area.getWidth(), args.area.getHeight(), args.justificationType); return ConfiguredArrangement { std::move (arrangement), {} }; }; GlyphArrangementCache::getInstance()->draw (*this, { context.getFont(), text, area, justificationType, useEllipsesIfTooBig }, std::move (configureArrangement)); } void Graphics::drawText (const String& text, Rectangle area, Justification justificationType, bool useEllipsesIfTooBig) const { drawText (text, area.toFloat(), justificationType, useEllipsesIfTooBig); } void Graphics::drawText (const String& text, int x, int y, int width, int height, Justification justificationType, const bool useEllipsesIfTooBig) const { drawText (text, coordsToRectangle (x, y, width, height), justificationType, useEllipsesIfTooBig); } void Graphics::drawFittedText (const String& text, Rectangle area, Justification justification, const int maximumNumberOfLines, const float minimumHorizontalScale) const { if (text.isEmpty() || area.isEmpty() || ! context.clipRegionIntersects (area)) return; struct ArrangementArgs { auto tie() const noexcept { return std::tie (font, text, area, justification, maximumNumberOfLines, minimumHorizontalScale); } bool operator< (const ArrangementArgs& other) const noexcept { return tie() < other.tie(); } const Font font; const String text; const Rectangle area; const Justification justification; const int maximumNumberOfLines; const float minimumHorizontalScale; }; auto configureArrangement = [] (const ArrangementArgs& args) { GlyphArrangement arrangement; arrangement.addFittedText (args.font, args.text, args.area.getX(), args.area.getY(), args.area.getWidth(), args.area.getHeight(), args.justification, args.maximumNumberOfLines, args.minimumHorizontalScale); return ConfiguredArrangement { std::move (arrangement), {} }; }; GlyphArrangementCache::getInstance()->draw (*this, { context.getFont(), text, area.toFloat(), justification, maximumNumberOfLines, minimumHorizontalScale }, std::move (configureArrangement)); } void Graphics::drawFittedText (const String& text, int x, int y, int width, int height, Justification justification, const int maximumNumberOfLines, const float minimumHorizontalScale) const { drawFittedText (text, coordsToRectangle (x, y, width, height), justification, maximumNumberOfLines, minimumHorizontalScale); } //============================================================================== void Graphics::fillRect (Rectangle r) const { context.fillRect (r, false); } void Graphics::fillRect (Rectangle r) const { context.fillRect (r); } void Graphics::fillRect (int x, int y, int width, int height) const { context.fillRect (coordsToRectangle (x, y, width, height), false); } void Graphics::fillRect (float x, float y, float width, float height) const { fillRect (coordsToRectangle (x, y, width, height)); } void Graphics::fillRectList (const RectangleList& rectangles) const { context.fillRectList (rectangles); } void Graphics::fillRectList (const RectangleList& rects) const { for (auto& r : rects) context.fillRect (r, false); } void Graphics::fillAll() const { fillRect (context.getClipBounds()); } void Graphics::fillAll (Colour colourToUse) const { if (! colourToUse.isTransparent()) { auto clip = context.getClipBounds(); context.saveState(); context.setFill (colourToUse); context.fillRect (clip, false); context.restoreState(); } } //============================================================================== void Graphics::fillPath (const Path& path) const { if (! (context.isClipEmpty() || path.isEmpty())) context.fillPath (path, AffineTransform()); } void Graphics::fillPath (const Path& path, const AffineTransform& transform) const { if (! (context.isClipEmpty() || path.isEmpty())) context.fillPath (path, transform); } void Graphics::strokePath (const Path& path, const PathStrokeType& strokeType, const AffineTransform& transform) const { Path stroke; strokeType.createStrokedPath (stroke, path, transform, context.getPhysicalPixelScaleFactor()); fillPath (stroke); } //============================================================================== void Graphics::drawRect (float x, float y, float width, float height, float lineThickness) const { drawRect (coordsToRectangle (x, y, width, height), lineThickness); } void Graphics::drawRect (int x, int y, int width, int height, int lineThickness) const { drawRect (coordsToRectangle (x, y, width, height), lineThickness); } void Graphics::drawRect (Rectangle r, int lineThickness) const { drawRect (r.toFloat(), (float) lineThickness); } void Graphics::drawRect (Rectangle r, const float lineThickness) const { jassert (r.getWidth() >= 0.0f && r.getHeight() >= 0.0f); RectangleList rects; rects.addWithoutMerging (r.removeFromTop (lineThickness)); rects.addWithoutMerging (r.removeFromBottom (lineThickness)); rects.addWithoutMerging (r.removeFromLeft (lineThickness)); rects.addWithoutMerging (r.removeFromRight (lineThickness)); context.fillRectList (rects); } //============================================================================== void Graphics::fillEllipse (Rectangle area) const { Path p; p.addEllipse (area); fillPath (p); } void Graphics::fillEllipse (float x, float y, float w, float h) const { fillEllipse (coordsToRectangle (x, y, w, h)); } void Graphics::drawEllipse (float x, float y, float width, float height, float lineThickness) const { drawEllipse (coordsToRectangle (x, y, width, height), lineThickness); } void Graphics::drawEllipse (Rectangle area, float lineThickness) const { Path p; if (area.getWidth() == area.getHeight()) { // For a circle, we can avoid having to generate a stroke p.addEllipse (area.expanded (lineThickness * 0.5f)); p.addEllipse (area.reduced (lineThickness * 0.5f)); p.setUsingNonZeroWinding (false); fillPath (p); } else { p.addEllipse (area); strokePath (p, PathStrokeType (lineThickness)); } } void Graphics::fillRoundedRectangle (float x, float y, float width, float height, float cornerSize) const { fillRoundedRectangle (coordsToRectangle (x, y, width, height), cornerSize); } void Graphics::fillRoundedRectangle (Rectangle r, const float cornerSize) const { Path p; p.addRoundedRectangle (r, cornerSize); fillPath (p); } void Graphics::drawRoundedRectangle (float x, float y, float width, float height, float cornerSize, float lineThickness) const { drawRoundedRectangle (coordsToRectangle (x, y, width, height), cornerSize, lineThickness); } void Graphics::drawRoundedRectangle (Rectangle r, float cornerSize, float lineThickness) const { Path p; p.addRoundedRectangle (r, cornerSize); strokePath (p, PathStrokeType (lineThickness)); } void Graphics::drawArrow (Line line, float lineThickness, float arrowheadWidth, float arrowheadLength) const { Path p; p.addArrow (line, lineThickness, arrowheadWidth, arrowheadLength); fillPath (p); } void Graphics::fillCheckerBoard (Rectangle area, float checkWidth, float checkHeight, Colour colour1, Colour colour2) const { jassert (checkWidth > 0 && checkHeight > 0); // can't be zero or less! if (checkWidth > 0 && checkHeight > 0) { context.saveState(); if (colour1 == colour2) { context.setFill (colour1); context.fillRect (area); } else { auto clipped = context.getClipBounds().getIntersection (area.getSmallestIntegerContainer()); if (! clipped.isEmpty()) { const int checkNumX = (int) (((float) clipped.getX() - area.getX()) / checkWidth); const int checkNumY = (int) (((float) clipped.getY() - area.getY()) / checkHeight); const float startX = area.getX() + (float) checkNumX * checkWidth; const float startY = area.getY() + (float) checkNumY * checkHeight; const float right = (float) clipped.getRight(); const float bottom = (float) clipped.getBottom(); for (int i = 0; i < 2; ++i) { int cy = i; RectangleList checks; for (float y = startY; y < bottom; y += checkHeight) for (float x = startX + (cy++ & 1) * checkWidth; x < right; x += checkWidth * 2.0f) checks.addWithoutMerging ({ x, y, checkWidth, checkHeight }); checks.clipTo (area); context.setFill (i == ((checkNumX ^ checkNumY) & 1) ? colour1 : colour2); context.fillRectList (checks); } } } context.restoreState(); } } //============================================================================== void Graphics::drawVerticalLine (const int x, float top, float bottom) const { if (top < bottom) context.fillRect (Rectangle ((float) x, top, 1.0f, bottom - top)); } void Graphics::drawHorizontalLine (const int y, float left, float right) const { if (left < right) context.fillRect (Rectangle (left, (float) y, right - left, 1.0f)); } void Graphics::drawLine (Line line) const { context.drawLine (line); } void Graphics::drawLine (float x1, float y1, float x2, float y2) const { context.drawLine (Line (x1, y1, x2, y2)); } void Graphics::drawLine (float x1, float y1, float x2, float y2, float lineThickness) const { drawLine (Line (x1, y1, x2, y2), lineThickness); } void Graphics::drawLine (Line line, const float lineThickness) const { Path p; p.addLineSegment (line, lineThickness); fillPath (p); } void Graphics::drawDashedLine (Line line, const float* dashLengths, int numDashLengths, float lineThickness, int n) const { jassert (n >= 0 && n < numDashLengths); // your start index must be valid! const Point delta ((line.getEnd() - line.getStart()).toDouble()); const double totalLen = delta.getDistanceFromOrigin(); if (totalLen >= 0.1) { const double onePixAlpha = 1.0 / totalLen; for (double alpha = 0.0; alpha < 1.0;) { jassert (dashLengths[n] > 0); // can't have zero-length dashes! const double lastAlpha = alpha; alpha += dashLengths [n] * onePixAlpha; n = (n + 1) % numDashLengths; if ((n & 1) != 0) { const Line segment (line.getStart() + (delta * lastAlpha).toFloat(), line.getStart() + (delta * jmin (1.0, alpha)).toFloat()); if (lineThickness != 1.0f) drawLine (segment, lineThickness); else context.drawLine (segment); } } } } //============================================================================== void Graphics::setImageResamplingQuality (const Graphics::ResamplingQuality newQuality) { saveStateIfPending(); context.setInterpolationQuality (newQuality); } //============================================================================== void Graphics::drawImageAt (const Image& imageToDraw, int x, int y, bool fillAlphaChannel) const { drawImageTransformed (imageToDraw, AffineTransform::translation ((float) x, (float) y), fillAlphaChannel); } void Graphics::drawImage (const Image& imageToDraw, Rectangle targetArea, RectanglePlacement placementWithinTarget, bool fillAlphaChannelWithCurrentBrush) const { if (imageToDraw.isValid()) drawImageTransformed (imageToDraw, placementWithinTarget.getTransformToFit (imageToDraw.getBounds().toFloat(), targetArea), fillAlphaChannelWithCurrentBrush); } void Graphics::drawImageWithin (const Image& imageToDraw, int dx, int dy, int dw, int dh, RectanglePlacement placementWithinTarget, bool fillAlphaChannelWithCurrentBrush) const { drawImage (imageToDraw, coordsToRectangle (dx, dy, dw, dh).toFloat(), placementWithinTarget, fillAlphaChannelWithCurrentBrush); } void Graphics::drawImage (const Image& imageToDraw, int dx, int dy, int dw, int dh, int sx, int sy, int sw, int sh, const bool fillAlphaChannelWithCurrentBrush) const { if (imageToDraw.isValid() && context.clipRegionIntersects (coordsToRectangle (dx, dy, dw, dh))) drawImageTransformed (imageToDraw.getClippedImage (coordsToRectangle (sx, sy, sw, sh)), AffineTransform::scale ((float) dw / (float) sw, (float) dh / (float) sh) .translated ((float) dx, (float) dy), fillAlphaChannelWithCurrentBrush); } void Graphics::drawImageTransformed (const Image& imageToDraw, const AffineTransform& transform, const bool fillAlphaChannelWithCurrentBrush) const { if (imageToDraw.isValid() && ! context.isClipEmpty()) { if (fillAlphaChannelWithCurrentBrush) { context.saveState(); context.clipToImageAlpha (imageToDraw, transform); fillAll(); context.restoreState(); } else { context.drawImage (imageToDraw, transform); } } } //============================================================================== Graphics::ScopedSaveState::ScopedSaveState (Graphics& g) : context (g) { context.saveState(); } Graphics::ScopedSaveState::~ScopedSaveState() { context.restoreState(); } } // namespace juce