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