/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. 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 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-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 { namespace { template Rectangle coordsToRectangle (Type x, Type y, Type w, Type h) noexcept { #if JUCE_DEBUG const int maxVal = 0x3fffffff; jassert ((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 }; } } //============================================================================== LowLevelGraphicsContext::LowLevelGraphicsContext() {} LowLevelGraphicsContext::~LowLevelGraphicsContext() {} //============================================================================== Graphics::Graphics (const Image& imageToDrawOnto) : context (*imageToDrawOnto.createLowLevelContext()), contextToDelete (&context) { jassert (imageToDrawOnto.isValid()); // Can't draw into a null image! } Graphics::Graphics (LowLevelGraphicsContext& internalContext) noexcept : context (internalContext) { } Graphics::~Graphics() { } //============================================================================== 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 (static_cast (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.isNotEmpty()) { // 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; GlyphArrangement arr; arr.addLineOfText (context.getFont(), text, (float) startX, (float) baselineY); if (flags != Justification::left) { auto w = arr.getBoundingBox (0, -1, true).getWidth(); if ((flags & (Justification::horizontallyCentred | Justification::horizontallyJustified)) != 0) w /= 2.0f; arr.draw (*this, AffineTransform::translation (-w, 0)); } else { arr.draw (*this); } } } void Graphics::drawMultiLineText (const String& text, const int startX, const int baselineY, const int maximumLineWidth) const { if (text.isNotEmpty() && startX < context.getClipBounds().getRight()) { GlyphArrangement arr; arr.addJustifiedText (context.getFont(), text, (float) startX, (float) baselineY, (float) maximumLineWidth, Justification::left); arr.draw (*this); } } void Graphics::drawText (const String& text, Rectangle area, Justification justificationType, bool useEllipsesIfTooBig) const { if (text.isNotEmpty() && context.clipRegionIntersects (area.getSmallestIntegerContainer())) { GlyphArrangement arr; arr.addCurtailedLineOfText (context.getFont(), text, 0.0f, 0.0f, area.getWidth(), useEllipsesIfTooBig); arr.justifyGlyphs (0, arr.getNumGlyphs(), area.getX(), area.getY(), area.getWidth(), area.getHeight(), justificationType); arr.draw (*this); } } 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.isNotEmpty() && (! area.isEmpty()) && context.clipRegionIntersects (area)) { GlyphArrangement arr; arr.addFittedText (context.getFont(), text, (float) area.getX(), (float) area.getY(), (float) area.getWidth(), (float) area.getHeight(), justification, maximumNumberOfLines, minimumHorizontalScale); arr.draw (*this); } } 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) ((clipped.getX() - area.getX()) / checkWidth); const int checkNumY = (int) ((clipped.getY() - area.getY()) / checkHeight); const float startX = area.getX() + checkNumX * checkWidth; const float startY = area.getY() + 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 (dw / (float) sw, 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