1 /**
2 Slider.
3 
4 Copyright: Copyright Auburn Sounds 2015-2017.
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: exp, abs;
11 
12 import dplug.core.math;
13 import dplug.gui.bufferedelement;
14 import dplug.client.params;
15 
16 
17 enum HandleStyle
18 {
19     shapeW,
20     shapeV,
21     shapeA,
22     shapeBlock
23 }
24 
25 class UISlider : UIBufferedElementPBR, IParameterListener
26 {
27 public:
28 nothrow:
29 @nogc:
30 
31     // Trail customization
32     @ScriptProperty L16 trailDepth = L16(30000);
33     @ScriptProperty RGBA unlitTrailDiffuse = RGBA(130, 90, 45, 5);
34     @ScriptProperty RGBA litTrailDiffuse = RGBA(240, 165, 102, 130);
35     @ScriptProperty float trailWidth = 0.2f;
36 
37     @ScriptProperty RGBA litTrailDiffuseAlt = RGBA(240, 165, 102, 130);
38     @ScriptProperty bool hasAlternateTrail = false;
39     @ScriptProperty float trailBase = 0.0f; // trail is from trailBase to parameter value
40 
41     // Handle customization
42     @ScriptProperty HandleStyle handleStyle = HandleStyle.shapeW;
43     @ScriptProperty float handleHeightRatio = 0.25f;
44     @ScriptProperty float handleWidthRatio = 0.7f;
45     @ScriptProperty RGBA handleDiffuse = RGBA(248, 245, 233, 16);
46     @ScriptProperty RGBA handleMaterial = RGBA(0, 255, 128, 255);
47 
48     this(UIContext context, Parameter param)
49     {
50         super(context, flagAnimated | flagPBR);
51         _param = param;
52         _param.addListener(this);
53         _sensivity = 1.0f;
54          _pushedAnimation = 0;
55         clearCrosspoints();
56         setCursorWhenDragged(MouseCursor.drag);
57         setCursorWhenMouseOver(MouseCursor.move);
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 onDrawBufferedPBR(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                 int alpha = 3 * litTrail.a / 2;
159                 if (alpha > 255)
160                     alpha = 255;
161                 litTrail.a = cast(ubyte) alpha;
162             }
163 
164             paintTrail(0, 1, unlitTrailDiffuse);
165             paintTrail(trailBase, value, litTrail);
166         }
167 
168         // Paint handle of slider
169         int emissive = handleDiffuse.a;
170         if (isMouseOver && !isDragged)
171             emissive += 50;
172         if (emissive > 255)
173             emissive = 255;
174 
175         RGBA handleDiffuseLit = RGBA(handleDiffuse.r, handleDiffuse.g, handleDiffuse.b, cast(ubyte)emissive);
176 
177         diffuseMap.cropImageRef(handleRect).fillAll(handleDiffuseLit);
178 
179         if (handleStyle == HandleStyle.shapeV)
180         {
181             auto c0 = L16(20000);
182             auto c1 = L16(50000);
183 
184             int h0 = handleRect.min.y;
185             int h1 = handleRect.center.y;
186             int h2 = handleRect.max.y;
187 
188             verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1);
189             verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0);
190         }
191         else if (handleStyle == HandleStyle.shapeA)
192         {
193             auto c0 = L16(50000);
194             auto c1 = L16(20000);
195 
196             int h0 = handleRect.min.y;
197             int h1 = handleRect.center.y;
198             int h2 = handleRect.max.y;
199 
200             verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1);
201             verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0);
202         }
203         else if (handleStyle == HandleStyle.shapeW)
204         {
205             auto c0 = L16(15000);
206             auto c1 = L16(65535);
207             auto c2 = L16(51400);
208 
209             int h0 = handleRect.min.y;
210             int h1 = (handleRect.min.y * 3 + handleRect.max.y + 2) / 4;
211             int h2 = handleRect.center.y;
212             int h3 = (handleRect.min.y + handleRect.max.y * 3 + 2) / 4;
213             int h4 = handleRect.max.y;
214 
215             verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1);
216             verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c2);
217             verticalSlope(depthMap, box2i(handleRect.min.x, h2, handleRect.max.x, h3), c2, c1);
218             verticalSlope(depthMap, box2i(handleRect.min.x, h3, handleRect.max.x, h4), c1, c0);
219         }
220         else if (handleStyle == HandleStyle.shapeBlock)
221         {
222             depthMap.cropImageRef(handleRect).fillAll(L16(50000));
223         }
224 
225         materialMap.cropImageRef(handleRect).fillAll(handleMaterial);
226 
227         // Fill opacity for handle
228         diffuseOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque);
229         depthOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque);
230         materialOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque);
231     }
232 
233     override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
234     {
235         // double-click => set to default
236         if (isDoubleClick || mstate.altPressed)
237         {
238             if (auto p = cast(FloatParameter)_param)
239             {
240                 p.beginParamEdit();
241                 p.setFromGUI(p.defaultValue());
242                 p.endParamEdit();
243             }
244             else if (auto p = cast(IntegerParameter)_param)
245             {
246                 p.beginParamEdit();
247                 p.setFromGUI(p.defaultValue());
248                 p.endParamEdit();
249             }
250             else
251                 assert(false); // only integer and float parameters supported
252         }
253 
254         return Click.startDrag; // to initiate dragging
255     }
256 
257     // Called when mouse drag this Element.
258     override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate)
259     {
260         // FUTURE: replace by actual trail height instead of total height
261         float displacementInHeight = cast(float)(dy) / _position.height;
262 
263         float modifier = 1.0f;
264         if (mstate.shiftPressed || mstate.ctrlPressed)
265             modifier *= 0.1f;
266 
267         double oldParamValue = _param.getNormalized() + _draggingDebt;
268         double newParamValue = oldParamValue - displacementInHeight * modifier * _sensivity;
269         if (mstate.altPressed)
270             newParamValue = _param.getNormalizedDefault();
271 
272         if (y > _mousePosOnLast0Cross)
273             return;
274         if (y < _mousePosOnLast1Cross)
275             return;
276 
277         if (newParamValue <= 0 && oldParamValue > 0)
278             _mousePosOnLast0Cross = y;
279 
280         if (newParamValue >= 1 && oldParamValue < 1)
281             _mousePosOnLast1Cross = y;
282 
283         if (newParamValue < 0)
284             newParamValue = 0;
285         if (newParamValue > 1)
286             newParamValue = 1;
287 
288         if (newParamValue > 0)
289             _mousePosOnLast0Cross = float.infinity;
290 
291         if (newParamValue < 1)
292             _mousePosOnLast1Cross = -float.infinity;
293 
294         if (newParamValue != oldParamValue)
295         {
296             if (auto p = cast(FloatParameter)_param)
297             {
298                 p.setFromGUINormalized(newParamValue);
299             }
300             else if (auto p = cast(IntegerParameter)_param)
301             {
302                 p.setFromGUINormalized(newParamValue);
303                 _draggingDebt = newParamValue - p.getNormalized();
304             }
305             else
306                 assert(false); // only integer and float parameters supported
307         }
308     }
309 
310     // For lazy updates
311     override void onBeginDrag()
312     {
313         _param.beginParamEdit();
314         setDirtyWhole();
315     }
316 
317     override void onStopDrag()
318     {
319         _param.endParamEdit();
320         setDirtyWhole();
321         _draggingDebt = 0.0f;
322     }
323 
324     override void onMouseEnter()
325     {
326         _param.beginParamHover();
327         setDirtyWhole();
328     }
329 
330     override void onMouseExit()
331     {
332         _param.endParamHover();
333         setDirtyWhole();
334     }
335 
336     override void onParameterChanged(Parameter sender)
337     {
338         setDirtyWhole();
339     }
340 
341     override void onBeginParameterEdit(Parameter sender)
342     {
343     }
344 
345     override void onEndParameterEdit(Parameter sender)
346     {
347     }
348 
349     override void onBeginParameterHover(Parameter sender)
350     {
351     }
352 
353     override void onEndParameterHover(Parameter sender)
354     {
355     }
356 
357 
358 protected:
359 
360     /// The parameter this switch is linked with.
361     Parameter _param;
362 
363     /// Sensivity: given a mouse movement in 100th of the height of the knob,
364     /// how much should the normalized parameter change.
365     float _sensivity;
366 
367     float  _pushedAnimation;
368 
369     float _mousePosOnLast0Cross;
370     float _mousePosOnLast1Cross;
371 
372     // Exists because small mouse drags for integer parameters may not
373     // lead to a parameter value change, hence a need to accumulate those drags.
374     float _draggingDebt = 0.0f;
375 
376     void clearCrosspoints()
377     {
378         _mousePosOnLast0Cross = float.infinity;
379         _mousePosOnLast1Cross = -float.infinity;
380     }
381 }