Overriding hitTest() to improve the RangeFader

Introduction

My previous implementation of RangeFader allows you to click and drag on areas other than Thumb, as shown below:

With a TwoValue slider, I think it would be more useful to make the non-Thumb parts of the slider not clickable or draggable, since the unintentional Thumb may be more responsive.

One way to disable this is to use a convenient member function called setSliderSnapsToMousePosition(), but the effect does not apply to TwoValue sliders. So, there is another valid way to override hitTest(). This is called to find out whether a particular x, y coordinate is considered to be inside the component or not. If you override this with the method described in this article, you can restrict clicking and dragging only around Thumb as shown below:

Prerequisites

There is nothing to prepare in advance in this article. We will start by creating a new project.

Creating a new project

First, create a new project with the name HitTestTutotrial.

Preparing the RangeFader

Create a RangeFader to be used in this article. PluginEditor.h is a bit messy, but since this is a tutorial project, we will not export it to a separate file. If you want to know more about the implementation of this RangeFader, please refer to this article I wrote earlier.

class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
    void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height,
                           float sliderPos,
                           float minSliderPos,
                           float maxSliderPos,
                           const juce::Slider::SliderStyle style, juce::Slider& slider) override
    {
        auto isTwoVal   = (style == juce::Slider::SliderStyle::TwoValueVertical   || style == juce::Slider::SliderStyle::TwoValueHorizontal);
        auto isThreeVal = (style == juce::Slider::SliderStyle::ThreeValueVertical || style == juce::Slider::SliderStyle::ThreeValueHorizontal);
        
        auto trackWidth = juce::jmin (2.0f, (float) height * 0.25f);

        juce::Point<float> startPoint (slider.isHorizontal() ? (float) x : (float) x + (float) width * 0.5f,
                                       slider.isHorizontal() ? (float) y + (float) height * 0.5f : (float) (height + y));

        juce::Point<float> endPoint (slider.isHorizontal() ? (float) (width + x) : startPoint.x,
                                     slider.isHorizontal() ? startPoint.y : (float) y);
     
        juce::Path backgroundTrack;
        backgroundTrack.startNewSubPath (startPoint);
        backgroundTrack.lineTo (endPoint);
        g.setColour (blackGrey);
        juce::PathStrokeType pathStrokeType (1.0);
        float dashedLength[2] = {2, 4};
        pathStrokeType.createDashedStroke(backgroundTrack, backgroundTrack, dashedLength, 2);
        g.strokePath(backgroundTrack, pathStrokeType);

        juce::Path valueTrack;
        juce::Point<float> minPoint, maxPoint, thumbPoint;

        if (isTwoVal || isThreeVal)
        {
            minPoint = { minSliderPos, (float) height * 0.5f };
            maxPoint = { maxSliderPos, (float) height * 0.5f };
            thumbPoint = { sliderPos, (float) height * 0.5f };
        }
        else
        {
            auto kx = slider.isHorizontal() ? sliderPos : ((float) x + (float) width * 0.5f);
            auto ky = slider.isHorizontal() ? ((float) y + (float) height * 0.5f) : sliderPos;

            minPoint = startPoint;
            maxPoint = { kx, ky };
        }

        auto minThumbWidth = getSliderThumbRadius (slider) - 22;
        auto maxThumbWidth = getSliderThumbRadius (slider) - 26;
        auto thumbWidth = getSliderThumbRadius (slider) - 19;
                        
        valueTrack.startNewSubPath (minPoint);
        valueTrack.lineTo (maxPoint);
        g.setColour ((isTwoVal || isThreeVal) ? grey : slider.findColour (juce::Slider::trackColourId));
        g.strokePath (valueTrack, { trackWidth, juce::PathStrokeType::curved, juce::PathStrokeType::rounded });
                
        if (! isTwoVal)
        {
            g.setColour (slider.findColour (juce::Slider::thumbColourId));
            g.fillEllipse (juce::Rectangle<float> (static_cast<float> (thumbWidth), static_cast<float> (thumbWidth)).withCentre (isThreeVal ? thumbPoint : maxPoint));
        }

        if (isTwoVal || isThreeVal)
        {
            int thumbBeingDragged = slider.getThumbBeingDragged();
                    
            if (thumbBeingDragged == 1 || thumbBeingDragged == 2)
            {
                slider.setMouseCursor (juce::MouseCursor::NoCursor);
                
                g.setColour (offWhite);
                g.drawRoundedRectangle (juce::Rectangle<float> (static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth),
                                                                static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth))
                                                                .withCentre (thumbBeingDragged == 1 ? maxPoint : minPoint), 50, 1.0);
                
                g.setColour (grey);
                g.fillEllipse (juce::Rectangle<float> (static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth),
                                                       static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth))
                                                       .withCentre (thumbBeingDragged == 1 ? minPoint : maxPoint));
            }
            else
            {
                slider.setMouseCursor (juce::MouseCursor::NormalCursor);
              
                g.setColour (offWhite);
                g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (minThumbWidth), static_cast<float> (minThumbWidth)).withCentre (minPoint), 50, 1.0);
                g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (maxThumbWidth), static_cast<float> (maxThumbWidth)).withCentre (maxPoint), 50, 1.0);

            }
            
            if (isThreeVal)
            {
                g.setColour (stYellow);
                g.fillEllipse (juce::Rectangle<float> (static_cast<float> (thumbWidth), static_cast<float> (thumbWidth)).withCentre (thumbPoint));
            }
        }
    }
    
private:
    juce::Colour blue = juce::Colour::fromFloatRGBA (0.42, 0.83, 1.0, 1.0);
    juce::Colour stYellow = juce::Colour::fromFloatRGBA (0.89, 0.90, 0.27, 1.0);
    juce::Colour offWhite = juce::Colour::fromFloatRGBA (0.83, 0.83, 0.89, 1.0);
    juce::Colour grey = juce::Colour::fromFloatRGBA (0.42, 0.42, 0.42, 1.0);
    juce::Colour blackGrey = juce::Colour::fromFloatRGBA (0.2, 0.2, 0.2, 1.0);
};

class RangeFader : public juce::Slider
{
public:
    RangeFader()
    {
        setSliderStyle (juce::Slider::TwoValueHorizontal);
        setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
        setLookAndFeel (&customLookAndFeel);
        setVelocityBasedMode (true);
        setVelocityModeParameters (0.5, 1, 0.09, false);
        setRange (0.0, 100.0, 0.5);
        setMinAndMaxValues (0.0, 100.0);
    }
    
private:
    CustomLookAndFeel customLookAndFeel;    
};

Next, we declare an object of the RangeFader class.

class HitTestTutorialAudioProcessorEditor  : public juce::AudioProcessorEditor
{
・・・
private:
    RangeFader rangeFader;

Then, all you have to do is to implement PluginEditor.cpp as shown below:

HitTestTutorialAudioProcessorEditor::HitTestTutorialAudioProcessorEditor (HitTestTutorialAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (400, 300);
    
    addAndMakeVisible (&rangeFader);
}
void HitTestTutorialAudioProcessorEditor::paint (juce::Graphics& g)
{
    juce::Colour black = juce::Colour::fromFloatRGBA (0.08, 0.08, 0.08, 1.0);
    g.fillAll (black);
}
void HitTestTutorialAudioProcessorEditor::resized()
{
    rangeFader.setBounds(5, getHeight() / 2, getWidth() - 10, 30);
}

If it has been built correctly, it should look like the image below. You can change the value by clicking or dragging anywhere except the Thumb area.

Overriding hitTest()

Now, override hitTest() in the RangeFader class as shown below:

class RangeFader : public juce::Slider
{
public:
・・・
    bool hitTest (int x, int y) override
    {
        int minPosX = getPositionOfValue(getMinValue());
        int maxPosX = getPositionOfValue(getMaxValue());
        int posY = getHeight() * 0.5f;
            
        juce::Point<int> minPoint {minPosX, posY};
        juce::Point<int> maxPoint {maxPosX, posY};
        
        return (minPoint.getDistanceFrom ({x, y}) <= 10 || maxPoint.getDistanceFrom ({x, y}) <= 10);
    }

getPositionOfValue() is a member function of the Slider class that returns the x, y coordinates of the value given as an argument. Since the RangeFader we have created is a horizontal style, this function returns the x coordinate of the value.

In addition, we have created Point type objects with these coordinates, minPoint and maxPoint, and implemented them to return True if the argument given to hitTest() is within the respective 10px radius.

Now let’s build and try clicking and dragging various parts of the RangeFader. You will see that you cannot click or drag anywhere except around the Thumb.

Conclusion

In this tutorial, I explained the steps to improve RangeFader by using hitTest() override. When I first learned about hitTest(), I had no idea how overriding it would change things. I hope this article will be of help to those who are struggling to understand this function. Thank you for reading to the end.

Comments

Copied title and URL