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