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