Range Slider

I had the need to develop a two value slider whereby users can move both min and max values at the same time by clicking on the slider's range and dragging. I'm posting the code here in case anyone might need to develop something similar.

p.s. my screen grabbing software doesn't pick up the mouse cursor, so you'll just have to imagine it's there :)

class RangeSlider : public Slider
{
    
public:
        RangeSlider(String text):
        Slider(text),
        thumbWidth(20)
        {
            setSliderStyle(SliderStyle::TwoValueHorizontal);
        }
        
        ~RangeSlider() {};

private:
        void mouseDown(const MouseEvent& event)
        {
            topThumbDownX = valueToProportionOfLength(getMinValue())*getWidth();
            bottomThumbDownX = valueToProportionOfLength(getMaxValue())*getWidth();
        }

        void mouseDrag(const MouseEvent& event)
        {
            const float sliderMin = getMinValue();
            const float sliderMax = getMaxValue();
            const int minPosition = valueToProportionOfLength(getMinimum())*getWidth();
            const int maxPosition = valueToProportionOfLength(getMaximum())*getWidth();
            const float currentMouseX = event.getPosition().getX();
            const float bottomThumbPosition = valueToProportionOfLength(sliderMin)*getWidth();
            const float topThumbPosition = valueToProportionOfLength(sliderMax)*getWidth();
            const int distanceFromStart = event.getDistanceFromDragStartX();
            
            if(currentMouseX>bottomThumbPosition+thumbWidth && currentMouseX<topThumbPosition-thumbWidth)
            {        
                if(topThumbPosition>minPosition && bottomThumbPosition<maxPosition)
                {
                setMinValue(proportionOfLengthToValue(topThumbDownX+distanceFromStart)/getWidth());        
                setMaxValue(proportionOfLengthToValue(bottomThumbDownX+distanceFromStart)/getWidth());    
                }
            }
            
            else if(event.getPosition().getY()>getHeight()/2.f)
            {
                if(currentMouseX>topThumbPosition-thumbWidth && currentMouseX<topThumbPosition+thumbWidth)
                    setMaxValue(proportionOfLengthToValue(currentMouseX/getWidth()));                                            
            }
            else
            {
                if(currentMouseX>bottomThumbPosition-thumbWidth && currentMouseX<bottomThumbPosition+thumbWidth)
                    setMinValue(proportionOfLengthToValue(currentMouseX/getWidth()));                
            }
            
        }
        
        int topThumbDownX, bottomThumbDownX;
        const int thumbWidth;
};
3 Likes

really cool, thanks! Great for a stereo panner!

Nice! :) Thanks for posting

Wery cool - could be used for altering range of midi notes passed through.

I just made a quick edit; turns out it's a better idea to check to see if the user is moving the range BEFORE checking to see if they are moving the min max values :)

Thank you.

I suggest to change

 setMinValue (proportionOfLengthToValue(topThumbDownX+distanceFromStart)/getWidth());        
 setMaxValue (proportionOfLengthToValue(bottomThumbDownX+distanceFromStart)/getWidth());

to

 setMinValue (proportionOfLengthToValue (float (topThumbDownX + distanceFromStart) / getWidth()));        
 setMaxValue (proportionOfLengthToValue (float (bottomThumbDownX + distanceFromStart) / getWidth()));

( put the getWidth() into the proportionOfLengthToValue(). Add a float convertion, otherwise the argument will always be 0.)

Additionaly, I have changed it to work with fast mouse movements. I also didn't like that you had to click to the upper half to set the min value and the lower half to set the max value.

Here is my take:

class RangeSlider  : public Slider
{
public:
    RangeSlider();
    
    ~RangeSlider();
    
private:
    void mouseDown (const MouseEvent& event) override;
    void mouseDrag (const MouseEvent& event) override;
    void valueChanged() override;

    bool mouseDragBetweenThumbs;
    float xMinAtThumbDown;
    float xMaxAtThumbDown;
};

 

RangeSlider::RangeSlider ()
  : mouseDragBetweenThumbs {false}
{
    setSliderStyle (Slider::TwoValueHorizontal);
}

RangeSlider::~RangeSlider ()
{
}

// To enable the section between the two thumbs to be draggable.
void RangeSlider::mouseDown (const MouseEvent& event)
{
    const float currentMouseX = event.getPosition().getX();
    const int thumbRadius = getLookAndFeel().getSliderThumbRadius (*this);
    xMinAtThumbDown = valueToProportionOfLength (getMinValue()) * getWidth();
    xMaxAtThumbDown = valueToProportionOfLength (getMaxValue()) * getWidth();

    if (currentMouseX > xMinAtThumbDown + thumbRadius && currentMouseX < xMaxAtThumbDown - thumbRadius)
    {
        mouseDragBetweenThumbs = true;
    }
    else
    {
        mouseDragBetweenThumbs = false;
        Slider::mouseDown (event);
    }
}

// To enable the section between the two thumbs to be draggable.
void RangeSlider::mouseDrag (const MouseEvent& event)
{
    const float distanceFromStart = event.getDistanceFromDragStartX();
    
    if (mouseDragBetweenThumbs)
    {        
        setMinValue (proportionOfLengthToValue ((xMinAtThumbDown + distanceFromStart) / getWidth()));   
        setMaxValue (proportionOfLengthToValue ((xMaxAtThumbDown + distanceFromStart) / getWidth()));    
    }
    else
    {
        Slider::mouseDrag (event);
    }  
}

// This makes one thumb slide if the other is moved against it.
void RangeSlider::valueChanged()
{   
    if (getMinValue() == getMaxValue())
    {
        const int minimalIntervalBetweenMinAndMax = 1;
        if (getMaxValue() + minimalIntervalBetweenMinAndMax <= getMaximum())
        {
            setMaxValue(getMaxValue() + minimalIntervalBetweenMinAndMax);
        }
        else
        {
            setMinValue(getMinValue() - minimalIntervalBetweenMinAndMax);
        }
    }
}

Nice one. A prime candidate for a Juce module?

Oh well… this, in my case, seems like the typical case that proves that it’s always a good idea to have a look at this forum before trying to re-invent a wheel.

I don’t know if it’s me being a noob (which I am) but I implemented the solution shared on the posts above in my project and I get an error that points to juce_NormalisableRange.h
//If you hit this assertion then either your normalisation function is not working
//correctly or your input is out of the expected bounds.
jassert (clampedValue == value);
The error is caused by
setMinValue (proportionOfLengthToValue ((xMinAtThumbDown + distanceFromStart) / getWidth()));
in the mouseDrag function.

By what I understand what triggers the error is that the ratio fed into the proportionOfLengthToValue method is outside the 0-1 bounds.

I got around this problem by doing this:

if (mouseDragBetweenThumbs)
{
double ratioMin = (xMinAtThumbDown + distanceFromStart) / getWidth();
double ratioMax = (xMaxAtThumbDown + distanceFromStart) / getWidth();
float minV = proportionOfLengthToValue ( min( max(ratioMin, 0.0), 1.0 ));
float maxV = proportionOfLengthToValue ( min( max(ratioMax, 0.0), 1.0 ));
if (minV > getMinimum())
setMinValue (minV);
if (maxV < getMaximum())
setMaxValue (maxV);
}
else
{ Slider::mouseDrag (event); }

If someone feels like improving the way this is achieved I can’t wait to read :partying_face:

Thanks for the update, seems reasonable to me. Can’t believe it’s 6 years since I posted that code :laughing: