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