Interconnected Slider values

I’m replicating EZRanger, but I’m getting a strange result.
Some preamble: The widget looks like this < r1:slider > < sli:slider > <r2:slider>

r1 is the left handle from sli, and r2 is the right handle from sli. Together they both specify a range with sli the handle that shifts both r1 & r2. A simple formulation is, since r1,r2 & sli have a domain of [ 0, 1 ], if the sliders are meant to range from 40 to 1600, then it’s a lerp of linlin(x,0,1,40,1600), though I’m using my own lerping function.

So, if sli is moved to the left, r1 is expected to follow at a specified distance, like all range handles. But, for some reason, using a mouse and dragging sli:slider to the left, the slider never ever hits the lowest point. It is as if the mouse loses focus somewhere near the end point occasionally. I have to use the mousewheel or arrow keys to move the handle to it’s lowest point. Some descrepancies with the results is down to the interpolation, but the handle movement behaviour, is this something that is built into Slider?

It’s a little hard to tell what’s going on in code from your description - are you layering three Sliders to get the desired effect?

Maybe some general advice around testing complex UI controls like this (sorry if this is obvious to you already :slight_smile: - but I see these problems come up a lot, so hopefully this is helpful for others as well).

  1. Make sure you have a clearly defined data model - meaning: storage for your values - that is not connected to your View objects and represents the MINIMAL representation of that data. Specifically, don’t include derived data like the range value in your model, just calculate it. So your model could be the min and max values, or min and range, but not all three. These can be as local or class member var’s, inside an Event or an Environment like (min: 20, max: 500).
  2. There are pros and cons to each option, but I would suggest storing your values as the “true” semantic value (e.g. 40…1600) rather than a normalized 0..1 value. In general, I find this makes code more readable and helps reduce the potential for errors.
  3. First test whether the UI is displaying values correctly. In your case, set your model values to e.g. ~min = 40; ~max = 100; and then display the UI and see if it shows what you expect. Change the value with your UI showing: does it update correctly? Show two copies of the UI: do they BOTH update correctly when you change the value?
  4. If display is working, then test that Slider action’s are giving what you expect (just print the values, no need to even hook them up). Can you drag each slider through it’s full range as expected? Is it producing the values you expect?
  5. Connect each slider.action, one at a time, so that they update your model. Since you’ve tested each piece of code you need for this individually (e.g. min first, then max). Depending on how you’re storing them, the range slider needs to update TWO values (min and max) so do this last once you’re confident with the others.
  6. Finally, hook up all three controls at once. Try spawning multiple copies of the UI and ensure that they all update when you change one value, either by dragging a slider OR by updating the model “manually”.

Incrementally testing like this should make it a lot easier to identify where your problems are creeping in.

Here’s the code: Apologies if there’s students + teachers here, if this was going to be someone’s homework assignment.

(
var n1,n2,r1,r2,sli,lay,getXfromY,getYfromX,d1,d2,l1,l2,sl;
var slivalue,r1value,r2value;
var diffstart,diffend;
var range;
var dir;
var start,end;
var tmp;


getYfromX={
        arg x,ystart,yend,xstart=0,xend=1;
        var res;
        res=getXfromY.(x,xstart,xend,ystart,yend);
        res;
};

getXfromY={   // leaf input [ 0-1 ]
        // convert y to 0 - 1
        arg y,ystart,yend,xstart=0,xend=1;
        var x;
        if(y==nil,{ // there is no default value, use the average
                y=0.5;
        });
        // x = (( y - ystart )(xend-xstart) /(yend -ystart)) + xstart;
        x=linlin(y,ystart,yend,xstart,xend);
        x;
};


start=0 ; end=1;
range=end-start;
dir=range.sign;
slivalue=0.5;
tmp=getXfromY.(start,start,end);
r1value=slivalue-tmp/2;
diffstart=getYfromX.(r1value,start,end);


a=getYfromX.(0,start,end);
"a:%\n".postf(a);

"watch1:tmp:% diffstart:%\n".postf(tmp,diffstart);
tmp=getXfromY.(end,start,end);
r2value=tmp-slivalue/2;
diffend=getYfromX.(r2value,start,end);


w=Window.new;
n1=NumberBox().value_(0);
n2=NumberBox().value_(1);
d1=NumberBox().value_(diffstart);
d2=NumberBox().value_(diffend);

l1=NumberBox().value_(start);
l2=NumberBox().value_(end);
sl=NumberBox().value_(0);

sli= Slider.new().orientation_(\horizontal).value_(0.5);
r1=Slider.new().orientation_(\horizontal).value_(0.25);
r2=Slider.new().orientation_(\horizontal).value_(0.75);

"slidersli:pixelstep:% step:%\n".postf(sli.pixelStep,sli.step);
sl.value=getYfromX.(0.5,start,end);

lay=VLayout(
        HLayout[ r1,r2 ],
        HLayout[ l1,l2],
        HLayout[ d1,sl,d2],
        HLayout [n1,sli,n2]
);


w.view.layout=lay;
w.front;

r1.action={
        |i|
        var res=i.value;
        var tstart,tmp;
        tmp=slivalue-res;
        case
        {tmp>=0}{
                diffstart=getYfromX.(tmp,start,end);// this adds +start
                tstart=getYfromX.(res,start,end);
                // tstart=getYfromX.(slivalue,start,end)-diffstart+start;
                "r1.action:slivalue:% tmp:% diffstart:% tstart:%\n".postf(slivalue,tmp,diffstart,tstart);
                n1.value=tstart;
                d1.value=diffstart;
                r1value=res;
        }
        {tmp<0}{
                r1.value=r1value;

        };


};


r2.action={
        |i|
        var res=i.value;
        var tend;
        diffend=res-slivalue;
        case
        {diffend>=0}{
                diffend=getYfromX.(diffend,start,end);
                tend=getYfromX.(res,start,end);
                // tend=getYfromX.(slivalue,start,end)+diffend;
                n2.value=tend;
                d2.value=diffend;
              r2value=res;
                }
        {diffend<0}{
                r2.value=r2value;

        };
};


sli.action={
        |i|
        var res=i.value;
        var tend,tstart;
        var confirm=0;
        var currvalue;
        currvalue=getYfromX.(res,start,end);
        tend=currvalue+diffend;
        tstart=currvalue-diffstart;
"slidersli:pixelstep:% step:%\n".postf(sli.pixelStep,sli.step);

        if(tstart>=start,{
                confirm=1;
        });
        if(tend<=end,{
                confirm=confirm+1;
        });

        if(confirm==2,{
                n1.value=tstart;
                n2.value=tend;
                // slivalue=getXfromY.(res,start,end);
                slivalue=res;
                sl.value=currvalue;
        },{
                "setting sli.value:%\n".postf(res);
                sli.value=slivalue;
        });
};


)

There’s probably some bounds checking that I didn’t add, but this is a snippet of the gui interaction. I made this because i didn’t want to input parameter ranges randomly, but explore which is a good working range.

Ok so, I tried keeping values at the canonical units, i.e what it is supposed to represent. But to keep approximation errors from accumulating, it’s advisable to work at the units that the widget provides by default, which is [ 0, 1 ], and to do a conversion as the final operation.

As the controls are interconnected, since how much one slider is set affects the main slider, step wise addition of components doesn’t quite make sense but having a minimum set of workable components before moving to the next set of constraints/features makes for better development.

So the bit that affects the slider movement is shifting the main slider to the edges. This is dependent on how fast the user gesture is, and so the slider stops just short of the limit of the range. To test this out, do a quick diagonal pull of Slider, the end point is slightly different from a horizontal drag, and also how fast the handle is pulled. With small numbers, this isn’t much problem, but with larger ranges of end,start tuples, that small number translates to a lets say 30 hz in a range of [ 40, 1600 ] hz.

In a weird bit of synergy, someone’s username here suggested a solution. To solve the occurrence of different stop points, having the program do an overshoot and clip based on a threshold value, would work. So instead of clipping to the previous value for every overshoot, clipping to the limit for a given threshold value might be better instead. Or maybe just clip to limit.