1 /**
2 Film-strip knob for a flat UI.
3 
4 Copyright: Guillaume Piolat 2015-2018.
5 Copyright: Ethan Reker 2017.
6 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 */
8 module dplug.flatwidgets.flatknob;
9 
10 import std.math;
11 import std.algorithm.comparison;
12 
13 import dplug.core.math;
14 
15 import dplug.gui.element;
16 
17 import dplug.client.params;
18 
19 /**
20 * UIFilmstripKnob is a knob with a vertical RGBA image describing the knob graphics.
21 */
22 class UIFilmstripKnob : UIElement, IParameterListener
23 {
24 public:
25 nothrow:
26 @nogc:
27 
28     // This will change to 1.0f at one point for consistency, so better express your knob
29     // sensivity with that.
30     enum defaultSensivity = 0.25f;
31 
32     this(UIContext context, Parameter param, OwnedImage!RGBA mipmap, int numFrames, float sensitivity = 0.25)
33     {
34         super(context, flagRaw);
35         _param = param;
36         _sensitivity = sensitivity;
37         _filmstrip = mipmap;
38         _numFrames = numFrames;
39         _knobWidth = _filmstrip.w;
40         _knobHeight = _filmstrip.h / _numFrames;
41         _param.addListener(this);
42         _disabled = false;
43         _filmstripScaled = mallocNew!(OwnedImage!RGBA)();
44         _frameNthResized.reallocBuffer(_numFrames);
45     }
46 
47     ~this()
48     {
49         _param.removeListener(this);
50         _filmstripScaled.destroyFree();
51         _frameNthResized.reallocBuffer(0);
52     }
53 
54     override void reflow()
55     {
56         int W = position.width;
57         int H = position.height * _numFrames;
58         if (_filmstripScaled.w != W || _filmstripScaled.h != H)
59         {
60             _filmstripScaled.size(position.width, position.height * _numFrames);
61             _frameNthResized[] = false;
62         }
63     }
64 
65     /// Returns: sensivity.
66     float sensivity()
67     {
68         return _sensitivity;
69     }
70 
71     /// Sets sensivity.
72     float sensivity(float sensitivity)
73     {
74         return _sensitivity = sensitivity;
75     }
76 
77     void disable()
78     {
79         _disabled = true;
80     }
81 
82     override void onDrawRaw(ImageRef!RGBA rawMap, box2i[] dirtyRects)
83     {
84         setCurrentImage();
85         ImageRef!RGBA _currentImage = _filmstripScaled.toRef().cropImageRef(box2i(_imageX1, _imageY1, _imageX2, _imageY2));
86         foreach(dirtyRect; dirtyRects)
87         {
88             ImageRef!RGBA croppedRawIn = _currentImage.cropImageRef(dirtyRect);
89             ImageRef!RGBA croppedRawOut = rawMap.cropImageRef(dirtyRect);
90 
91             int w = dirtyRect.width;
92             int h = dirtyRect.height;
93 
94             for(int j = 0; j < h; ++j)
95             {
96                 RGBA[] input = croppedRawIn.scanline(j);
97                 RGBA[] output = croppedRawOut.scanline(j);
98 
99                 for(int i = 0; i < w; ++i)
100                 {
101                     ubyte alpha = input[i].a;
102 
103                     RGBA color = blendColor(input[i], output[i], alpha);
104                     output[i] = color;
105                 }
106             }
107 
108         }
109     }
110 
111     float distance(float x1, float x2, float y1, float y2)
112     {
113         return sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
114     }
115 
116     void setCurrentImage()
117     {
118         float value = _param.getNormalized();
119         currentFrame = cast(int)(round(value * (_numFrames - 1)));
120 
121         if(currentFrame < 0) currentFrame = 0;
122 
123         _imageX1 = 0;
124         _imageY1 = (_filmstripScaled.h / _numFrames) * currentFrame;
125 
126         _imageX2 = _filmstripScaled.w;
127         _imageY2 = _imageY1 + (_filmstripScaled.h / _numFrames);
128 
129         cacheImage(currentFrame);
130     }
131 
132     override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
133     {
134         if (!containsPoint(x, y))
135             return Click.unhandled;
136 
137         // double-click => set to default
138         if (isDoubleClick || mstate.altPressed)
139         {
140             _param.beginParamEdit();
141             if (auto fp = cast(FloatParameter)_param)
142                 fp.setFromGUI(fp.defaultValue());
143             else if (auto ip = cast(IntegerParameter)_param)
144                 ip.setFromGUI(ip.defaultValue());
145             else
146                 assert(false);
147             _param.endParamEdit();
148         }
149 
150         _normalizedValueWhileDragging = _param.getNormalized();
151 
152         return Click.startDrag; // to initiate dragging
153     }
154 
155     // Called when mouse drag this Element.
156     override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate)
157     {
158         if(!_disabled)
159         {
160             float displacementInHeight = cast(float)(dy) / _position.height;
161 
162             float modifier = 1.0f;
163             if (mstate.shiftPressed || mstate.ctrlPressed)
164                 modifier *= 0.1f;
165 
166             double oldParamValue = _normalizedValueWhileDragging;
167             double newParamValue = oldParamValue - displacementInHeight * modifier * _sensitivity;
168             if (mstate.altPressed)
169                 newParamValue = _param.getNormalizedDefault();
170 
171             if (y > _mousePosOnLast0Cross)
172                 return;
173             if (y < _mousePosOnLast1Cross)
174                 return;
175 
176             if (newParamValue <= 0 && oldParamValue > 0)
177                 _mousePosOnLast0Cross = y;
178 
179             if (newParamValue >= 1 && oldParamValue < 1)
180                 _mousePosOnLast1Cross = y;
181 
182             if (newParamValue < 0)
183                 newParamValue = 0;
184             if (newParamValue > 1)
185                 newParamValue = 1;
186 
187             if (newParamValue > 0)
188                 _mousePosOnLast0Cross = float.infinity;
189 
190             if (newParamValue < 1)
191                 _mousePosOnLast1Cross = -float.infinity;
192 
193             if (newParamValue != oldParamValue)
194             {
195                 if (auto fp = cast(FloatParameter)_param)
196                     fp.setFromGUINormalized(newParamValue);
197                 else if (auto ip = cast(IntegerParameter)_param)
198                     ip.setFromGUINormalized(newParamValue);
199                 else
200                     assert(false);
201                 _normalizedValueWhileDragging = newParamValue;
202             }
203         }
204     }
205 
206     // For lazy updates
207     override void onBeginDrag()
208     {
209         _param.beginParamEdit();
210         setDirtyWhole();
211     }
212 
213     override void onStopDrag()
214     {
215         _param.endParamEdit();
216         clearCrosspoints();
217         setDirtyWhole();
218     }
219 
220     override void onMouseMove(int x, int y, int dx, int dy, MouseState mstate)
221     {
222     }
223 
224     override void onMouseEnter()
225     {
226         _param.beginParamHover();
227     }
228 
229     override void onMouseExit()
230     {
231         _param.endParamHover();
232     }
233 
234     override void onParameterChanged(Parameter sender) nothrow @nogc
235     {
236         setDirtyWhole();
237     }
238 
239     override void onBeginParameterEdit(Parameter sender)
240     {
241     }
242 
243     override void onEndParameterEdit(Parameter sender)
244     {
245     }
246 
247     override void onBeginParameterHover(Parameter sender)
248     {
249     }
250 
251     override void onEndParameterHover(Parameter sender)
252     {
253     }
254 
255 protected:
256 
257     /// The parameter this knob is linked with.
258     Parameter _param;
259 
260     OwnedImage!RGBA _filmstrip;
261     OwnedImage!RGBA _filmstripScaled;
262 
263     ImageRef!RGBA _currentImage;
264 
265     int _numFrames;
266     int _imageX1, _imageX2, _imageY1, _imageY2;
267     int currentFrame;
268 
269     int _knobWidth;
270     int _knobHeight;
271 
272     float _pushedAnimation;
273 
274     /// Sensivity: given a mouse movement in 100th of the height of the knob,
275     /// how much should the normalized parameter change.
276     float _sensitivity;
277 
278     float _mousePosOnLast0Cross;
279     float _mousePosOnLast1Cross;
280 
281     // Normalized value last set while dragging. Necessary for integer paramerers 
282     // that may round that normalized value when setting the parameter.
283     float _normalizedValueWhileDragging; 
284 
285     bool _disabled;
286 
287     ImageResizer _resizer;
288     bool[] _frameNthResized;
289 
290     void clearCrosspoints() nothrow @nogc
291     {
292         _mousePosOnLast0Cross = float.infinity;
293         _mousePosOnLast1Cross = -float.infinity;
294     }
295 
296     final bool containsPoint(int x, int y) nothrow @nogc
297     {
298         vec2f center = getCenter();
299         return vec2f(x, y).distanceTo(center) < getRadius();
300     }
301 
302     /// Returns: largest square centered in _position
303     final box2i getSubsquare() pure const nothrow @nogc
304     {
305         // We'll draw entirely in the largest centered square in _position.
306         box2i subSquare;
307         if (_position.width > _position.height)
308         {
309             int offset = (_position.width - _position.height) / 2;
310             int minX = offset;
311             subSquare = box2i(minX, 0, minX + _position.height, _position.height);
312         }
313         else
314         {
315             int offset = (_position.height - _position.width) / 2;
316             int minY = offset;
317             subSquare = box2i(0, minY, _position.width, minY + _position.width);
318         }
319         return subSquare;
320     }
321 
322     final float getRadius() pure const nothrow @nogc
323     {
324         return getSubsquare().width * 0.5f;
325     }
326 
327     final vec2f getCenter() pure const nothrow @nogc
328     {
329         box2i subSquare = getSubsquare();
330         float centerx = (subSquare.min.x + subSquare.max.x - 1) * 0.5f;
331         float centery = (subSquare.min.y + subSquare.max.y - 1) * 0.5f;
332         return vec2f(centerx, centery);
333     }
334 
335     // Resize sub-image lazily. Instead of doing it in `reflow()` for all frames,
336     // we do it in a case by case basis when needed in `onDrawRaw`.
337     void cacheImage(int frame)
338     {
339         // Limitation: the source image should have identically sized sub-frames.
340         assert(_filmstrip.h % _numFrames == 0);
341 
342         if (_frameNthResized[frame]) 
343             return;
344 
345         int hsource = _filmstrip.h / _numFrames;
346         int hdest   = _filmstripScaled.h / _numFrames;
347 
348         // Note: in order to avoid slight sample offsets, each subframe needs to be resized separately.
349         ImageRef!RGBA source     = _filmstrip.toRef.cropImageRef(rectangle(0, hsource * frame, _filmstrip.w, hsource));
350         ImageRef!RGBA dest = _filmstripScaled.toRef.cropImageRef(rectangle(0, hdest   * frame, _filmstripScaled.w, hdest  ));
351         _resizer.resizeImage_sRGBNoAlpha(source, dest);
352 
353         _frameNthResized[frame] = true;
354     }
355 }