/*
  ==============================================================================

   This file is part of the JUCE framework examples.
   Copyright (c) Raw Material Software Limited

   The code included in this file is provided under the terms of the ISC license
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
   to use, copy, modify, and/or distribute this software for any purpose with or
   without fee is hereby granted provided that the above copyright notice and
   this permission notice appear in all copies.

   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
   REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
   AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
   INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
   LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
   OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
   PERFORMANCE OF THIS SOFTWARE.

  ==============================================================================
*/

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             GraphicsDemo
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Showcases various graphics features.

 dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics
 exporters:        xcode_mac, vs2022, vs2026, linux_make, androidstudio,
                   xcode_iphone

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             Component
 mainClass:        GraphicsDemo

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include "../Assets/DemoUtilities.h"

//==============================================================================
/** Holds the various toggle buttons for the animation modes. */
class ControllersComponent final : public Component
{
public:
    ControllersComponent()
    {
        setOpaque (true);

        initialiseToggle (animatePosition, "Animate Position",  true);
        initialiseToggle (animateRotation, "Animate Rotation",  true);
        initialiseToggle (animateSize,     "Animate Size",      false);
        initialiseToggle (animateShear,    "Animate Shearing",  false);
        initialiseToggle (animateAlpha,    "Animate Alpha",     false);
        initialiseToggle (clipToRectangle, "Clip to Rectangle", false);
        initialiseToggle (clipToPath,      "Clip to Path",      false);
        initialiseToggle (clipToImage,     "Clip to Image",     false);
        initialiseToggle (quality,         "Higher quality image interpolation", false);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
    }

    void resized() override
    {
        auto r = getLocalBounds().reduced (4);

        int buttonHeight = 22;

        auto columns = r.removeFromTop (buttonHeight * 4);
        auto col = columns.removeFromLeft (200);

        animatePosition.setBounds (col.removeFromTop (buttonHeight));
        animateRotation.setBounds (col.removeFromTop (buttonHeight));
        animateSize    .setBounds (col.removeFromTop (buttonHeight));
        animateShear   .setBounds (col.removeFromTop (buttonHeight));

        columns.removeFromLeft (20);
        col = columns.removeFromLeft (200);

        animateAlpha   .setBounds (col.removeFromTop (buttonHeight));
        clipToRectangle.setBounds (col.removeFromTop (buttonHeight));
        clipToPath     .setBounds (col.removeFromTop (buttonHeight));
        clipToImage    .setBounds (col.removeFromTop (buttonHeight));

        r.removeFromBottom (6);
        quality.setBounds (r.removeFromTop (buttonHeight));
    }

    void initialiseToggle (ToggleButton& b, const char* name, bool on)
    {
        addAndMakeVisible (b);
        b.setButtonText (name);
        b.setToggleState (on, dontSendNotification);
    }

    ToggleButton animateRotation, animatePosition, animateAlpha, animateSize, animateShear;
    ToggleButton clipToRectangle, clipToPath, clipToImage, quality;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControllersComponent)
};

//==============================================================================
class GraphicsDemoBase : public Component
{
public:
    GraphicsDemoBase (ControllersComponent* cc, const String& name)
        : Component (name),
          controls (cc)
    {
        displayFont = FontOptions (Font::getDefaultMonospacedFontName(), 12.0f, Font::bold);
    }

    AffineTransform getTransform()
    {
        auto hw = 0.5f * (float) getWidth();
        auto hh = 0.5f * (float) getHeight();

        AffineTransform t;

        if (controls != nullptr && controls->animateRotation.getToggleState())
            t = t.rotated (rotation.getValue() * MathConstants<float>::twoPi);

        if (controls != nullptr && controls->animateSize.getToggleState())
            t = t.scaled (0.3f + size.getValue() * 2.0f);

        if (controls != nullptr && controls->animatePosition.getToggleState())
            t = t.translated (hw + hw * (offsetX.getValue() - 0.5f),
                              hh + hh * (offsetY.getValue() - 0.5f));
        else
            t = t.translated (hw, hh);

        if (controls != nullptr && controls->animateShear.getToggleState())
            t = t.sheared (shear.getValue() * 2.0f - 1.0f, 0.0f);

        return t;
    }

    float getAlpha() const
    {
        if (controls != nullptr && controls->animateAlpha.getToggleState())
            return alpha.getValue();

        return 1.0f;
    }

    void paint (Graphics& g) override
    {
        auto startTime = 0.0;

        {
            // A ScopedSaveState will return the Graphics context to the state it was at the time of
            // construction when it goes out of scope. We use it here to avoid clipping the fps text
            const Graphics::ScopedSaveState state (g);

            if (controls != nullptr && controls->clipToRectangle.getToggleState())  clipToRectangle (g);
            if (controls != nullptr && controls->clipToPath     .getToggleState())  clipToPath (g);
            if (controls != nullptr && controls->clipToImage    .getToggleState())  clipToImage (g);

            g.setImageResamplingQuality (controls != nullptr && controls->quality.getToggleState() ? Graphics::highResamplingQuality
                                                                                                   : Graphics::mediumResamplingQuality);

            // take a note of the time before the render
            startTime = Time::getMillisecondCounterHiRes();

            // then let the demo draw itself..
            drawDemo (g);
        }

        if (controls == nullptr)
            return;

        auto now = Time::getMillisecondCounterHiRes();
        auto filtering = 0.08;

        auto elapsedMs = now - startTime;
        averageTimeMs += (elapsedMs - averageTimeMs) * filtering;

        auto sinceLastRender = now - lastRenderStartTime;
        lastRenderStartTime = now;

        auto effectiveFPS = 1000.0 / averageTimeMs;
        auto actualFPS = sinceLastRender > 0 ? (1000.0 / sinceLastRender) : 0;
        averageActualFPS += (actualFPS - averageActualFPS) * filtering;

        GlyphArrangement ga;
        ga.addFittedText (displayFont,
                          "Time: " + String (averageTimeMs, 2)
                            + " ms\nEffective FPS: " + String (effectiveFPS, 1)
                            + "\nActual FPS: " + String (averageActualFPS, 1),
                          0, 10.0f, (float) getWidth() - 10.0f, (float) getHeight(), Justification::topRight, 3);

        g.setColour (Colours::white.withAlpha (0.5f));
        g.fillRect (ga.getBoundingBox (0, ga.getNumGlyphs(), true).getSmallestIntegerContainer().expanded (4));

        g.setColour (Colours::black);
        ga.draw (g);
    }

    virtual void drawDemo (Graphics&) = 0;

    void clipToRectangle (Graphics& g)
    {
        auto w = getWidth()  / 2;
        auto h = getHeight() / 2;

        auto x = (int) ((float) w * clipRectX.getValue());
        auto y = (int) ((float) h * clipRectY.getValue());

        g.reduceClipRegion (x, y, w, h);
    }

    void clipToPath (Graphics& g)
    {
        auto pathSize = (float) jmin (getWidth(), getHeight());

        Path p;
        p.addStar (Point<float> (clipPathX.getValue(),
                                 clipPathY.getValue()) * pathSize,
                   7,
                   pathSize * (0.5f + clipPathDepth.getValue()),
                   pathSize * 0.5f,
                   clipPathAngle.getValue());

        g.reduceClipRegion (p, AffineTransform());
    }

    void clipToImage (Graphics& g)
    {
        if (! clipImage.isValid())
            createClipImage();

        AffineTransform transform (AffineTransform::translation ((float) clipImage.getWidth()  / -2.0f,
                                                                 (float) clipImage.getHeight() / -2.0f)
                                   .rotated (clipImageAngle.getValue() * MathConstants<float>::twoPi)
                                   .scaled (2.0f + clipImageSize.getValue() * 3.0f)
                                   .translated ((float) getWidth()  * 0.5f,
                                                (float) getHeight() * 0.5f));

        g.reduceClipRegion (clipImage, transform);
    }

    void createClipImage()
    {
        clipImage = Image (Image::ARGB, 300, 300, true);

        Graphics g (clipImage);

        g.setGradientFill (ColourGradient (Colours::transparentBlack, 0, 0,
                                           Colours::black, 0, 300, false));

        for (int i = 0; i < 20; ++i)
            g.fillRect (Random::getSystemRandom().nextInt (200),
                        Random::getSystemRandom().nextInt (200),
                        Random::getSystemRandom().nextInt (100),
                        Random::getSystemRandom().nextInt (100));
    }

    //==============================================================================
    ControllersComponent* controls;

    SlowerBouncingNumber offsetX, offsetY, rotation, size, shear, alpha, clipRectX,
                         clipRectY, clipPathX, clipPathY, clipPathDepth, clipPathAngle,
                         clipImageX, clipImageY, clipImageAngle, clipImageSize;

    double lastRenderStartTime = 0.0, averageTimeMs = 0.0, averageActualFPS = 0.0;
    Image clipImage;
    Font displayFont { FontOptions{} };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GraphicsDemoBase)
};

//==============================================================================
class RectangleFillTypesDemo final : public GraphicsDemoBase
{
public:
    RectangleFillTypesDemo (ControllersComponent* cc)
        : GraphicsDemoBase (cc, "Fill Types: Rectangles")
    {}

    void drawDemo (Graphics& g) override
    {
        g.addTransform (getTransform());

        const int rectSize = jmin (getWidth(), getHeight()) / 2 - 20;

        g.setColour (colour1.withAlpha (getAlpha()));
        g.fillRect (-rectSize, -rectSize, rectSize, rectSize);

        g.setGradientFill (ColourGradient (colour1, 10.0f, (float) -rectSize,
                                           colour2, 10.0f + (float) rectSize, 0.0f, false));
        g.setOpacity (getAlpha());
        g.fillRect (10, -rectSize, rectSize, rectSize);

        g.setGradientFill (ColourGradient (colour1, (float) rectSize * -0.5f, 10.0f + (float) rectSize * 0.5f,
                                           colour2, 0, 10.0f + (float) rectSize, true));
        g.setOpacity (getAlpha());
        g.fillRect (-rectSize, 10, rectSize, rectSize);

        g.setGradientFill (ColourGradient (colour1, 10.0f, 10.0f,
                                           colour2, 10.0f + (float) rectSize, 10.0f + (float) rectSize, false));
        g.setOpacity (getAlpha());
        g.drawRect (10, 10, rectSize, rectSize, 5);
    }

    Colour colour1 { Colours::red }, colour2 { Colours::green };
};

//==============================================================================
class PathsDemo final : public GraphicsDemoBase
{
public:
    PathsDemo (ControllersComponent* cc, bool linear, bool radial)
        : GraphicsDemoBase (cc, String ("Paths") + (radial ? ": Radial Gradients"
                                                           : (linear ? ": Linear Gradients"
                                                                     : ": Solid"))),
          useLinearGradient (linear), useRadialGradient (radial)
    {
        logoPath = getJUCELogoPath();

        // rescale the logo path so that it's centred about the origin and has the right size.
        logoPath.applyTransform (RectanglePlacement (RectanglePlacement::centred)
                                 .getTransformToFit (logoPath.getBounds(),
                                                     Rectangle<float> (-120.0f, -120.0f, 240.0f, 240.0f)));

        // Surround it with some other shapes..
        logoPath.addStar ({ -300.0f, -50.0f }, 7, 30.0f, 70.0f, 0.1f);
        logoPath.addStar ({ 300.0f, 50.0f }, 6, 40.0f, 70.0f, 0.1f);
        logoPath.addEllipse (-100.0f, 150.0f, 200.0f, 140.0f);
        logoPath.addRectangle (-100.0f, -280.0f, 200.0f, 140.0f);
    }

    void drawDemo (Graphics& g) override
    {
        auto& p = logoPath;

        if (useLinearGradient || useRadialGradient)
        {
            Colour c1 (gradientColours[0].getValue(), gradientColours[1].getValue(), gradientColours[2].getValue(), 1.0f);
            Colour c2 (gradientColours[3].getValue(), gradientColours[4].getValue(), gradientColours[5].getValue(), 1.0f);
            Colour c3 (gradientColours[6].getValue(), gradientColours[7].getValue(), gradientColours[8].getValue(), 1.0f);

            auto x1 = gradientPositions[0].getValue() * (float) getWidth()  * 0.25f;
            auto y1 = gradientPositions[1].getValue() * (float) getHeight() * 0.25f;
            auto x2 = gradientPositions[2].getValue() * (float) getWidth()  * 0.75f;
            auto y2 = gradientPositions[3].getValue() * (float) getHeight() * 0.75f;

            ColourGradient gradient (c1, x1, y1,
                                     c2, x2, y2,
                                     useRadialGradient);

            gradient.addColour (gradientIntermediate.getValue(), c3);

            g.setGradientFill (gradient);
        }
        else
        {
            g.setColour (Colours::blue);
        }

        g.setOpacity (getAlpha());
        g.fillPath (p, getTransform());
    }

    Path logoPath;
    bool useLinearGradient, useRadialGradient;
    SlowerBouncingNumber gradientColours[9], gradientPositions[4], gradientIntermediate;
};

//==============================================================================
class StrokesDemo final : public GraphicsDemoBase
{
public:
    StrokesDemo (ControllersComponent* cc)
        : GraphicsDemoBase (cc, "Paths: Stroked")
    {}

    void drawDemo (Graphics& g) override
    {
        auto w = (float) getWidth();
        auto h = (float) getHeight();

        Path p;
        p.startNewSubPath (points[0].getValue() * w,
                           points[1].getValue() * h);

        for (int i = 2; i < numElementsInArray (points); i += 4)
            p.quadraticTo (points[i]    .getValue() * w,
                           points[i + 1].getValue() * h,
                           points[i + 2].getValue() * w,
                           points[i + 3].getValue() * h);

        p.closeSubPath();

        PathStrokeType stroke (0.5f + 10.0f * thickness.getValue());
        g.setColour (Colours::purple.withAlpha (getAlpha()));
        g.strokePath (p, stroke, AffineTransform());
    }

    SlowerBouncingNumber points[2 + 4 * 8], thickness;
};

//==============================================================================
class ImagesRenderingDemo final : public GraphicsDemoBase
{
public:
    ImagesRenderingDemo (ControllersComponent* cc, bool argb, bool tiled)
        : GraphicsDemoBase (cc, String ("Images") + (argb ? ": ARGB" : ": RGB") + (tiled ? " Tiled" : String() )),
          isArgb (argb), isTiled (tiled)
    {
        argbImage = getImageFromAssets ("juce_icon.png");
        rgbImage  = getImageFromAssets ("portmeirion.jpg");
    }

    void drawDemo (Graphics& g) override
    {
        auto image = isArgb ? argbImage : rgbImage;

        AffineTransform transform (AffineTransform::translation ((float) (image.getWidth()  / -2),
                                                                 (float) (image.getHeight() / -2))
                                   .followedBy (getTransform()));

        if (isTiled)
        {
            FillType fill (image, transform);
            fill.setOpacity (getAlpha());
            g.setFillType (fill);
            g.fillAll();
        }
        else
        {
            g.setOpacity (getAlpha());
            g.drawImageTransformed (image, transform, false);
        }
    }

    bool isArgb, isTiled;
    Image rgbImage, argbImage;
};

//==============================================================================
class GlyphsDemo final : public GraphicsDemoBase
{
public:
    GlyphsDemo (ControllersComponent* cc)
        : GraphicsDemoBase (cc, "Glyphs")
    {
        glyphs.addFittedText (FontOptions { 20.0f }, "The Quick Brown Fox Jumps Over The Lazy Dog",
                              -120, -50, 240, 100, Justification::centred, 2, 1.0f);
    }

    void drawDemo (Graphics& g) override
    {
        g.setColour (Colours::black.withAlpha (getAlpha()));
        glyphs.draw (g, getTransform());
    }

    GlyphArrangement glyphs;
};

//==============================================================================
class SVGDemo final : public GraphicsDemoBase
{
public:
    SVGDemo (ControllersComponent* cc)
        : GraphicsDemoBase (cc, "SVG")
    {
        createSVGDrawable();
    }

    void drawDemo (Graphics& g) override
    {
        if (Time::getCurrentTime().toMilliseconds() > lastSVGLoadTime.toMilliseconds() + 2000)
            createSVGDrawable();

        svgDrawable->draw (g, getAlpha(), getTransform());
    }

    void createSVGDrawable()
    {
        lastSVGLoadTime = Time::getCurrentTime();

        ZipFile icons { createAssetInputStream ("icons.zip") };

        // Load a random SVG file from our embedded icons.zip file.
        const std::unique_ptr<InputStream> svgFileStream (icons.createStreamForEntry (Random::getSystemRandom().nextInt (icons.getNumEntries())));

        if (svgFileStream.get() != nullptr)
        {
            svgDrawable = Drawable::createFromImageDataStream (*svgFileStream);

            if (svgDrawable != nullptr)
            {
                // to make our icon the right size, we'll set its bounding box to the size and position that we want.

                if (auto comp = dynamic_cast<DrawableComposite*> (svgDrawable.get()))
                    comp->setBoundingBox ({ -100.0f, -100.0f, 200.0f, 200.0f });
            }
        }
    }

    Time lastSVGLoadTime;
    std::unique_ptr<Drawable> svgDrawable;
};

class BlurDemo final : public GraphicsDemoBase
{
public:
    BlurDemo (ControllersComponent* cc)
        : GraphicsDemoBase (cc, "Blur")
    {
        image.setBackupEnabled (false);
    }

    void drawDemo (Graphics& g) override
    {
        const auto a = lopassA.next (jmap (frequencyA.getValue(), 0.09f, 0.12f));
        const auto b = lopassB.next (jmap (frequencyB.getValue(), 0.09f, 0.12f));

        initialPhase += 0.01f;
        initialPhase -= (float) (int) initialPhase;
        const auto startAngle = initialPhase * MathConstants<float>::twoPi;
        const auto centreSquare = image.getBounds().reduced (100);

        {
            Graphics g2 { image };
            g2.setColour (Colours::transparentBlack);
            g2.excludeClipRegion (centreSquare);
            g2.getInternalContext().fillRect (image.getBounds(), true);
        }

        if (auto ptr = image.getClippedImage (centreSquare).getPixelData())
        {
            ptr->applyGaussianBlurEffect (7.0f);
            ptr->multiplyAllAlphas (0.98f);
        }

        {
            Graphics g2 { image };
            const auto baseColour = Colours::cyan;
            const auto destColour = Colours::magenta;
            const auto offset = image.getBounds().getCentre().toFloat();
            const auto numSegments = 200;

            for (auto i = 0; i < numSegments; ++i)
            {
                g2.setColour (baseColour.interpolatedWith (destColour, (float) i / numSegments));

                const auto getPoint = [&] (auto ind)
                {
                    return offset + Point { 200 * std::sin (startAngle + a * (float) ind),
                                            200 * std::cos (startAngle + b * (float) ind) };
                };

                g2.drawLine ({ getPoint (i), getPoint (i + 1) }, 2.0f);
            }
        }

        AffineTransform transform (AffineTransform::translation ((float) (-image.getWidth()  / 2),
                                                                 (float) (-image.getHeight() / 2))
                                   .followedBy (getTransform()));

        g.setOpacity (getAlpha());
        g.drawImageTransformed (image, transform, false);
    }

    class Lopass
    {
    public:
        float next (float f) { return value += (f - value) * 0.05f; }

    private:
        float value{};
    };

    Image image { Image::ARGB, 512, 512, true };
    SlowerBouncingNumber frequencyA, frequencyB;
    Lopass lopassA, lopassB;
    float initialPhase = 0.0f;
};

//==============================================================================
class LinesDemo final : public GraphicsDemoBase
{
public:
    LinesDemo (ControllersComponent* cc)
        : GraphicsDemoBase (cc, "Lines")
    {}

    void drawDemo (Graphics& g) override
    {
        const auto bounds = getLocalBounds().toFloat();

        g.addTransform (AffineTransform::translation (-bounds.getWidth() / 2.0f,
                                                      -bounds.getHeight() / 2.0f)
                            .followedBy (getTransform()));

        // There are two reasons to use a transparency layer here instead of
        // setting the alpha of each colour.
        //
        //  1. To prevent semi-transparent lines occurring between rectangles in
        //     the D2D renderer, each rectangle has been expanded in size so
        //     there is a small overlap. A transparency layer prevents this
        //     overlap from becoming visible when applying an alpha.
        //
        //  2. In the CoreGraphics renderer when applying an alpha, or clipping
        //     to a path or image, lines would occur between rectangles that
        //     were not resolved by expanding the rectangles size. This appears
        //     to be a bug in CoreGraphics. However, drawing the changes into a
        //     transparency layer appears to avoid the issue.

        g.beginTransparencyLayer (getAlpha());
        ScopeGuard scope { [&] { g.endTransparencyLayer(); }};

        const auto lineWidth = 19 * thickness.getValue() + 1.0f;

        {
            lines.clear();

            const auto pos = offset.getValue();

            for (auto x = 0.0f; x < bounds.getWidth(); x += lineWidth)
            {
                const auto y = bounds.getHeight() * 0.3f;
                const auto length = y * std::abs (std::sin (x / 100.0f + 2.0f * pos));
                const auto width = jmin (lineWidth, bounds.getRight() - x);
                const Rectangle<float> lineBounds { x, y - length * 0.5f, width, length };
                lines.addWithoutMerging (lineBounds.expanded (0.5f, 0.0f));
            }

            g.setColour (Colours::blue);
            g.fillRectList (lines);
        }

        {
            lines.clear();

            const auto pos = offset.getValue();

            for (auto y = 0.0f; y < bounds.getWidth(); y += lineWidth)
            {
                const auto x = bounds.getWidth() * 0.3f;
                const auto length = x * std::abs (std::sin (y / 100.0f + 2.0f * pos));
                const auto width = jmin (lineWidth, bounds.getBottom() - y);
                const Rectangle<float> lineBounds { x - length * 0.5f, y, length, width };
                lines.addWithoutMerging (lineBounds.expanded (0.0f, 0.5f));
            }

            g.setGradientFill (ColourGradient (Colours::green,
                                               bounds.getTopLeft(),
                                               Colours::yellow,
                                               bounds.withWidth (bounds.getWidth() / 2.0f).getBottomRight(), false));
            g.fillRectList (lines);
        }

        g.setColour (Colours::red);

        g.drawLine (positions[0].getValue() * bounds.getWidth(),
                    positions[1].getValue() * bounds.getHeight(),
                    positions[2].getValue() * bounds.getWidth(),
                    positions[3].getValue() * bounds.getHeight());

        g.drawLine (positions[4].getValue() * bounds.getWidth(),
                    positions[5].getValue() * bounds.getHeight(),
                    positions[6].getValue() * bounds.getWidth(),
                    positions[7].getValue() * bounds.getHeight(),
                    10.0f * thickness.getValue());
    }

    SlowerBouncingNumber offset, positions[8], thickness;
    RectangleList<float> lines;
};

//==============================================================================
class ShapesDemo final : public GraphicsDemoBase
{
public:
    explicit ShapesDemo (ControllersComponent* cc)
        : GraphicsDemoBase (cc, "Shapes")
    {}

    void drawDemo (Graphics& g) override
    {
        auto bounds = getLocalBounds().toFloat();
        const auto rowHeight = bounds.getHeight() / 2.0f;
        const auto spacing = 5.0f;

        const auto drawShapes = [&] (auto area)
        {
            const auto lineThickness = thickness.getValue() * 25.0f;
            const auto cornerSize = 15.0f;
            const auto shapeWidth = area.getWidth() / 4.0f;

            g.drawEllipse (area.removeFromLeft (shapeWidth).reduced (spacing), lineThickness);
            g.fillEllipse (area.removeFromLeft (shapeWidth).reduced (spacing));

            g.drawRoundedRectangle (area.removeFromLeft (shapeWidth).reduced (spacing), cornerSize, lineThickness);
            g.fillRoundedRectangle (area.removeFromLeft (shapeWidth).reduced (spacing), cornerSize);
        };

        g.addTransform (AffineTransform::translation (-(bounds.getWidth() / 2.0f), -(bounds.getHeight() / 2.0f)).followedBy (getTransform()));

        g.setColour (juce::Colours::red.withAlpha (getAlpha()));
        drawShapes (bounds.removeFromTop (rowHeight).reduced (spacing));

        const auto r = bounds.removeFromTop (rowHeight).reduced (spacing);
        g.setGradientFill (juce::ColourGradient (juce::Colours::green, r.getTopLeft(),
                                                 juce::Colours::blue, r.getBottomRight(),
                                                 false));
        g.setOpacity (getAlpha());
        drawShapes (r);
    }

    SlowerBouncingNumber thickness;
};

//==============================================================================
class DemoHolderComponent final : public Component
{
public:
    DemoHolderComponent()
    {
        setOpaque (true);
    }

    void paint (Graphics& g) override
    {
        g.fillCheckerBoard (getLocalBounds().toFloat(), 48.0f, 48.0f,
                            Colours::lightgrey, Colours::white);
    }

    void setDemo (GraphicsDemoBase* newDemo)
    {
        if (currentDemo != nullptr)
            removeChildComponent (currentDemo);

        currentDemo = newDemo;

        if (currentDemo != nullptr)
        {
            addAndMakeVisible (currentDemo);
            resized();
        }
    }

    void resized() override
    {
        if (currentDemo != nullptr)
            currentDemo->setBounds (getLocalBounds());
    }

private:
    GraphicsDemoBase* currentDemo = nullptr;
    VBlankAttachment vblank { this, [&]
    {
        if (currentDemo != nullptr)
            currentDemo->repaint();
    }};
};

//==============================================================================
class TestListComponent final : public Component,
                                private ListBoxModel
{
public:
    TestListComponent (DemoHolderComponent& holder, ControllersComponent* controls)
        : demoHolder (holder)
    {
        demos.add (new PathsDemo (controls, false, true));
        demos.add (new PathsDemo (controls, true,  false));
        demos.add (new PathsDemo (controls, false, false));
        demos.add (new RectangleFillTypesDemo (controls));
        demos.add (new StrokesDemo (controls));
        demos.add (new ImagesRenderingDemo (controls, false, false));
        demos.add (new ImagesRenderingDemo (controls, false, true));
        demos.add (new ImagesRenderingDemo (controls, true,  false));
        demos.add (new ImagesRenderingDemo (controls, true,  true));
        demos.add (new BlurDemo   (controls));
        demos.add (new GlyphsDemo (controls));
        demos.add (new SVGDemo    (controls));
        demos.add (new LinesDemo  (controls));
        demos.add (new ShapesDemo (controls));

        addAndMakeVisible (listBox);
        listBox.setTitle ("Test List");
        listBox.setModel (this);
        listBox.selectRow (0);
    }

    void resized() override
    {
        listBox.setBounds (getLocalBounds());
    }

    int getNumRows() override
    {
        return demos.size();
    }

    void paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) override
    {
        if (demos[rowNumber] == nullptr)
            return;

        if (rowIsSelected)
            g.fillAll (Colour::contrasting (findColour (ListBox::textColourId),
                                            findColour (ListBox::backgroundColourId)));

        g.setColour (findColour (ListBox::textColourId));
        g.setFont (14.0f);
        g.drawFittedText (getNameForRow (rowNumber), 8, 0, width - 10, height, Justification::centredLeft, 2);
    }

    String getNameForRow (int rowNumber) override
    {
        if (auto* demo = demos[rowNumber])
            return demo->getName();

        return {};
    }

    void selectedRowsChanged (int lastRowSelected) override
    {
        demoHolder.setDemo (demos [lastRowSelected]);
    }

private:
    DemoHolderComponent& demoHolder;
    ListBox listBox;
    OwnedArray<GraphicsDemoBase> demos;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestListComponent)
};

//==============================================================================
class GraphicsDemo final : public Component
{
public:
    GraphicsDemo()
        : testList (demoHolder, &controllersComponent)
    {
        setName ("Graphics demo");
        setOpaque (true);

        addAndMakeVisible (demoHolder);
        addAndMakeVisible (controllersComponent);
        addAndMakeVisible (performanceDisplay);
        addAndMakeVisible (testList);

        setSize (750, 750);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (Colours::grey);
    }

    void resized() override
    {
        auto area = getLocalBounds();

        controllersComponent.setBounds (area.removeFromBottom (150));
        testList            .setBounds (area.removeFromRight (150));
        demoHolder          .setBounds (area);
        performanceDisplay  .setBounds (area.removeFromTop (20).removeFromRight (100));
    }

private:
    ControllersComponent controllersComponent;
    DemoHolderComponent demoHolder;
    Label performanceDisplay;
    TestListComponent testList;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GraphicsDemo)
};
