1 /**
2 * PBR widget: sliders. 
3 *
4 * Copyright: Copyright Auburn Sounds 2015 and later.
5 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 * Authors:   Guillaume Piolat
7 */
8 module dplug.pbrwidgets.slider;
9 
10 import std.math;
11 import std.algorithm.comparison;
12 
13 import dplug.core.math;
14 import dplug.graphics.drawex;
15 import dplug.gui.bufferedelement;
16 import dplug.client.params;
17 
18 
19 enum HandleStyle
20 {
21     shapeW,
22     shapeV,
23     shapeA,
24     shapeBlock
25 }
26 
27 class UISlider : UIBufferedElement, IParameterListener
28 {
29 public:
30 nothrow:
31 @nogc:
32 
33     // Trail customization
34     L16 trailDepth = L16(30000);
35     RGBA unlitTrailDiffuse = RGBA(130, 90, 45, 5);
36     RGBA litTrailDiffuse = RGBA(240, 165, 102, 130);
37     float trailWidth = 0.2f;
38 
39     RGBA litTrailDiffuseAlt = RGBA(240, 165, 102, 130);
40     bool hasAlternateTrail = false;
41     float trailBase = 0.0f; // trail is from trailBase to parameter value
42 
43     // Handle customization
44     HandleStyle handleStyle = HandleStyle.shapeW;
45     float handleHeightRatio = 0.25f;
46     float handleWidthRatio = 0.7f;
47     RGBA handleDiffuse = RGBA(248, 245, 233, 16);
48     RGBA handleMaterial = RGBA(0, 255, 128, 255);
49 
50     this(UIContext context, Parameter param)
51     {
52         super(context);
53         _param = param;
54         _param.addListener(this);
55         _sensivity = 1.0f;
56          _pushedAnimation = 0;
57         clearCrosspoints();
58     }
59 
60     ~this()
61     {
62         _param.removeListener(this);
63     }
64 
65     /// Returns: sensivity.
66     float sensivity()
67     {
68         return _sensivity;
69     }
70 
71     /// Sets sensivity.
72     float sensivity(float sensivity)
73     {
74         return _sensivity = sensivity;
75     }
76 
77     override void onAnimate(double dt, double time) nothrow @nogc
78     {
79         float target = isDragged() ? 1 : 0;
80         float newAnimation = lerp(_pushedAnimation, target, 1.0 - exp(-dt * 30));
81         if (abs(newAnimation - _pushedAnimation) > 0.001f)
82         {
83             _pushedAnimation = newAnimation;
84             setDirtyWhole();
85         }
86     }
87 
88     override void onDrawBuffered(ImageRef!RGBA diffuseMap, 
89                                  ImageRef!L16 depthMap, 
90                                  ImageRef!RGBA materialMap,
91                                  ImageRef!L8 diffuseOpacity,
92                                  ImageRef!L8 depthOpacity,
93                                  ImageRef!L8 materialOpacity) nothrow @nogc
94     {
95         int width = _position.width;
96         int height = _position.height;
97         int handleHeight = cast(int)(0.5f + this.handleHeightRatio * height * (1 - 0.12f * _pushedAnimation));
98         int handleWidth = cast(int)(0.5f + this.handleWidthRatio * width * (1 - 0.06f * _pushedAnimation));
99         int trailWidth = cast(int)(0.5f + width * this.trailWidth);
100 
101         int handleHeightUnpushed = cast(int)(0.5f + this.handleHeightRatio * height);
102         int trailMargin = cast(int)(0.5f + (handleHeightUnpushed - trailWidth) * 0.5f);
103         if (trailMargin < 0) 
104             trailMargin = 0;
105 
106         int trailX = cast(int)(0.5 + (width - trailWidth) * 0.5f);
107         int trailHeight = height - 2 * trailMargin;
108 
109         // The switch is in a subrect
110 
111         box2i holeRect =  box2i(trailX, trailMargin, trailX + trailWidth, trailMargin + trailHeight);
112 
113         float value = _param.getNormalized();
114 
115         int posX = cast(int)(0.5f + (width - handleWidth) * 0.5f);
116         int posY = cast(int)(0.5f + (1 - value) * (height - handleHeight));
117         assert(posX >= 0);
118         assert(posY >= 0);
119 
120         box2i handleRect = box2i(posX, posY, posX + handleWidth, posY + handleHeight);
121 
122 
123         // Dig hole and paint trail deeper hole
124         {
125 
126 
127             depthMap.cropImageRef(holeRect).fillAll(trailDepth);
128 
129             // Fill opacity for hole
130             diffuseOpacity.cropImageRef(holeRect).fillAll(opacityFullyOpaque);
131             depthOpacity.cropImageRef(holeRect).fillAll(opacityFullyOpaque);
132 
133             int valueToTrail(float value) nothrow @nogc
134             {
135                 return cast(int)(0.5f + (1 - value) * (height+4 - handleHeight) + handleHeight*0.5f - 2);
136             }
137 
138             void paintTrail(float from, float to, RGBA diffuse) nothrow @nogc
139             {
140                 int ymin = valueToTrail(from);
141                 int ymax = valueToTrail(to);
142                 if (ymin > ymax)
143                 {
144                     int temp = ymin;
145                     ymin = ymax;
146                     ymax = temp;
147                 }
148                 box2i b = box2i(holeRect.min.x, ymin, holeRect.max.x, ymax);
149                 diffuseMap.cropImageRef(b).fillAll(diffuse);
150             }
151 
152             
153         
154             RGBA litTrail = (value >= trailBase) ? litTrailDiffuse : litTrailDiffuseAlt;                  
155             if (isDragged)
156             {
157                 // lit trail is 50% brighter when dragged      
158                 litTrail.a = cast(ubyte) std.algorithm.min(255, 3 * litTrail.a / 2);
159             }
160 
161             paintTrail(0, 1, unlitTrailDiffuse);
162             paintTrail(trailBase, value, litTrail);
163         }
164 
165         // Paint handle of slider
166         int emissive = handleDiffuse.a;
167         if (isMouseOver && !isDragged)
168             emissive += 50;
169         if (emissive > 255)
170             emissive = 255;
171 
172         RGBA handleDiffuseLit = RGBA(handleDiffuse.r, handleDiffuse.g, handleDiffuse.b, cast(ubyte)emissive);        
173 
174         diffuseMap.cropImageRef(handleRect).fillAll(handleDiffuseLit);
175 
176         if (handleStyle == HandleStyle.shapeV)
177         {
178             auto c0 = L16(20000);
179             auto c1 = L16(50000);
180 
181             int h0 = handleRect.min.y;
182             int h1 = handleRect.center.y;
183             int h2 = handleRect.max.y;
184 
185             verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1);
186             verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0);
187         }
188         else if (handleStyle == HandleStyle.shapeA)
189         {
190             auto c0 = L16(50000);
191             auto c1 = L16(20000);
192 
193             int h0 = handleRect.min.y;
194             int h1 = handleRect.center.y;
195             int h2 = handleRect.max.y;
196 
197             verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1);
198             verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0);
199         }
200         else if (handleStyle == HandleStyle.shapeW)
201         {
202             auto c0 = L16(15000);
203             auto c1 = L16(65535);
204             auto c2 = L16(51400);
205 
206             int h0 = handleRect.min.y;
207             int h1 = (handleRect.min.y * 3 + handleRect.max.y + 2) / 4;
208             int h2 = handleRect.center.y;
209             int h3 = (handleRect.min.y + handleRect.max.y * 3 + 2) / 4;
210             int h4 = handleRect.max.y;
211 
212             verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1);
213             verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c2);
214             verticalSlope(depthMap, box2i(handleRect.min.x, h2, handleRect.max.x, h3), c2, c1);
215             verticalSlope(depthMap, box2i(handleRect.min.x, h3, handleRect.max.x, h4), c1, c0);
216         }
217         else if (handleStyle == HandleStyle.shapeBlock)
218         {
219             depthMap.cropImageRef(handleRect).fillAll(L16(50000));
220         }
221 
222         materialMap.cropImageRef(handleRect).fillAll(handleMaterial);
223 
224         // Fill opacity for handle
225         diffuseOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque);
226         depthOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque);
227         materialOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque);
228     }
229 
230     override bool onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
231     {
232         // double-click => set to default
233         if (isDoubleClick)
234         {
235             if (auto p = cast(FloatParameter)_param)
236             {
237                 p.beginParamEdit();
238                 p.setFromGUI(p.defaultValue());
239                 p.endParamEdit();
240             }
241             else if (auto p = cast(IntegerParameter)_param)
242             {
243                 p.beginParamEdit();
244                 p.setFromGUI(p.defaultValue());
245                 p.endParamEdit();
246             }
247             else
248                 assert(false); // only integer and float parameters supported
249         }
250 
251         return true; // to initiate dragging
252     }
253 
254     // Called when mouse drag this Element.
255     override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate)
256     {
257         // FUTURE: replace by actual trail height instead of total height
258         float displacementInHeight = cast(float)(dy) / _position.height; 
259 
260         float modifier = 1.0f;
261         if (mstate.shiftPressed || mstate.ctrlPressed)
262             modifier *= 0.1f;
263 
264         double oldParamValue = _param.getNormalized() + _draggingDebt;
265         double newParamValue = oldParamValue - displacementInHeight * modifier * _sensivity;
266 
267         if (y > _mousePosOnLast0Cross)
268             return;
269         if (y < _mousePosOnLast1Cross)
270             return;
271 
272         if (newParamValue <= 0 && oldParamValue > 0)
273             _mousePosOnLast0Cross = y;
274 
275         if (newParamValue >= 1 && oldParamValue < 1)
276             _mousePosOnLast1Cross = y;
277 
278         if (newParamValue < 0)
279             newParamValue = 0;
280         if (newParamValue > 1)
281             newParamValue = 1;
282 
283         if (newParamValue > 0)
284             _mousePosOnLast0Cross = float.infinity;
285 
286         if (newParamValue < 1)
287             _mousePosOnLast1Cross = -float.infinity;
288 
289         if (newParamValue != oldParamValue)
290         {
291             if (auto p = cast(FloatParameter)_param)
292             {
293                 p.setFromGUINormalized(newParamValue);
294             }
295             else if (auto p = cast(IntegerParameter)_param)
296             {
297                 p.setFromGUINormalized(newParamValue);
298                 _draggingDebt = newParamValue - p.getNormalized();
299             }
300             else
301                 assert(false); // only integer and float parameters supported
302         }
303     }
304 
305     // For lazy updates
306     override void onBeginDrag()
307     {
308         _param.beginParamEdit();
309         setDirtyWhole();
310     }
311 
312     override void onStopDrag()
313     {
314         _param.endParamEdit();
315         setDirtyWhole();
316         _draggingDebt = 0.0f;
317     }
318 
319     override void onMouseEnter()
320     {
321         setDirtyWhole();
322     }
323 
324     override void onMouseExit()
325     {
326         setDirtyWhole();
327     }
328 
329     override void onParameterChanged(Parameter sender)
330     {
331         setDirtyWhole();
332     }
333 
334     override void onBeginParameterEdit(Parameter sender)
335     {
336     }
337 
338     override void onEndParameterEdit(Parameter sender)
339     {
340     }
341 
342 protected:
343 
344     /// The parameter this switch is linked with.
345     Parameter _param;
346 
347     /// Sensivity: given a mouse movement in 100th of the height of the knob,
348     /// how much should the normalized parameter change.
349     float _sensivity;
350 
351     float  _pushedAnimation;
352 
353     float _mousePosOnLast0Cross;
354     float _mousePosOnLast1Cross;
355 
356     // Exists because small mouse drags for integer parameters may not 
357     // lead to a parameter value change, hence a need to accumulate those drags.
358     float _draggingDebt = 0.0f;
359 
360     void clearCrosspoints()
361     {
362         _mousePosOnLast0Cross = float.infinity;
363         _mousePosOnLast1Cross = -float.infinity;
364     }
365 }