1 /**
2 Film-strip slider.
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 
9 module dplug.flatwidgets.flatslider;
10 
11 import std.math;
12 import std.algorithm.comparison;
13 
14 import dplug.core.math;
15 import dplug.gui.bufferedelement;
16 import dplug.client.params;
17 
18 class UIFilmstripSlider : UIElement, IParameterListener
19 {
20 public:
21 nothrow:
22 @nogc:
23 
24     enum Direction
25     {
26         vertical,
27         horizontal
28     }
29 
30     @ScriptProperty Direction direction = Direction.vertical;
31 
32     this(UIContext context, FloatParameter param, OwnedImage!RGBA sliderImage, int numFrames, float sensitivity = 0.25)
33     {
34         super(context, flagRaw);
35         _param = param;
36         _param.addListener(this);
37         _sensivity = sensitivity;
38 
39         // Borrow original image.
40         _filmstrip = sliderImage;
41         _numFrames = numFrames;
42         _frameHeightOrig = _filmstrip.h / _numFrames;
43 
44         _disabled = false;
45         _filmstripResized = mallocNew!(OwnedImage!RGBA)();
46 
47         _frameNthResized.reallocBuffer(_numFrames);
48     }
49 
50     ~this()
51     {
52         _frameNthResized.reallocBuffer(0);
53         destroyFree(_filmstripResized);
54         _param.removeListener(this);
55     }
56 
57     /// Returns: sensivity.
58     float sensivity()
59     {
60         return _sensivity;
61     }
62 
63     /// Sets sensivity.
64     float sensivity(float sensivity)
65     {
66         return _sensivity = sensivity;
67     }
68 
69     override void onDrawRaw(ImageRef!RGBA rawMap, box2i[] dirtyRects)
70     {
71         assert(position.width != 0);
72         assert(position.height != 0); // does this hold though?
73 
74         // Get frame coordinate in _filmstripResized
75         float value = _param.getNormalized();
76         int frame = cast(int)(round(value * (_numFrames - 1)));
77         if(frame >= _numFrames) 
78             frame = _numFrames - 1;
79 
80         if(frame < 0) 
81             frame = 0;
82 
83         assert(frame >= 0 && frame < _numFrames);
84         assert(_filmstripResized.h == position.height * _numFrames);
85 
86         cacheImage(frame);
87 
88         int frameHeightResized = _filmstripResized.h / _numFrames;
89 
90         int x1 = 0;
91         int y1 = frameHeightResized * frame;
92         
93         assert(y1 + position.height <= _filmstripResized.h);
94 
95         box2i resizedRect = rectangle(x1, y1, position.width, position.height);
96         ImageRef!RGBA frameImage = _filmstripResized.toRef.cropImageRef(resizedRect);
97 
98         foreach(dirtyRect; dirtyRects)
99         {
100             ImageRef!RGBA croppedImage = frameImage.cropImageRef(dirtyRect);
101             ImageRef!RGBA croppedRaw = rawMap.cropImageRef(dirtyRect);
102 
103             int w = dirtyRect.width;
104             int h = dirtyRect.height;
105 
106             for(int j = 0; j < h; ++j)
107             {
108                 const(RGBA)* input = croppedImage.scanline(j).ptr;
109                 RGBA* output       = croppedRaw.scanline(j).ptr;
110 
111                 for(int i = 0; i < w; ++i)
112                 {
113                     ubyte alpha = input[i].a;
114                     output[i] = blendColor(input[i], output[i], alpha);
115                 }
116             }
117         }
118     }
119 
120     override void reflow()
121     {
122         // If target size is position.width x position.height, then
123         // slider image must be resized to position.width x (position.height x _numFrames).
124 
125         int usefulInputPixelHeight = _numFrames * _frameHeightOrig;
126         assert(usefulInputPixelHeight <= _filmstrip.h);
127 
128         box2i origRect = rectangle(0, 0, _filmstrip.w, usefulInputPixelHeight);        
129         ImageRef!RGBA originalInput = _filmstrip.toRef().cropImageRef(origRect);
130 
131         int W = position.width;
132         int H = position.height * _numFrames;
133         if (_filmstripResized.w != W || _filmstripResized.h != H)
134         {
135             _filmstripResized.size(W, H); // drawback, this still uses memory for slider graphics
136             _frameNthResized[] = false;
137         }   
138     }  
139 
140     override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
141     {
142         // double-click => set to default
143         if (isDoubleClick || mstate.altPressed)
144         {
145             _param.beginParamEdit();
146             if (auto p = cast(FloatParameter)_param)
147                 p.setFromGUI(p.defaultValue());
148             else if (auto p = cast(IntegerParameter)_param)
149                 p.setFromGUI(p.defaultValue());
150             else
151                 assert(false); // only integer and float parameters supported
152             _param.endParamEdit();
153         }
154 
155         return Click.startDrag; // to initiate dragging
156     }
157 
158     // Called when mouse drag this Element.
159     override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate)
160     {
161         // FUTURE: replace by actual trail height instead of total height
162         if(!_disabled)
163         {
164             float referenceCoord;
165             float displacementInHeight;
166             if (direction == Direction.vertical)
167             {
168                 referenceCoord = y;
169                 displacementInHeight = cast(float)(dy) / _position.height;
170             }
171             else
172             {
173                 referenceCoord = -x;
174                 displacementInHeight = cast(float)(-dx) / _position.width;
175             }
176 
177             float modifier = 1.0f;
178             if (mstate.shiftPressed || mstate.ctrlPressed)
179                 modifier *= 0.1f;
180 
181             double oldParamValue = _param.getNormalized() + _draggingDebt;
182             double newParamValue = oldParamValue - displacementInHeight * modifier * _sensivity;
183             if (mstate.altPressed)
184                 newParamValue = _param.getNormalizedDefault();
185 
186             if (referenceCoord > _mousePosOnLast0Cross)
187                 return;
188             if (referenceCoord < _mousePosOnLast1Cross)
189                 return;
190 
191             if (newParamValue <= 0 && oldParamValue > 0)
192                 _mousePosOnLast0Cross = referenceCoord;
193 
194             if (newParamValue >= 1 && oldParamValue < 1)
195                 _mousePosOnLast1Cross = referenceCoord;
196 
197             if (newParamValue < 0)
198                 newParamValue = 0;
199             if (newParamValue > 1)
200                 newParamValue = 1;
201 
202             if (newParamValue > 0)
203                 _mousePosOnLast0Cross = float.infinity;
204 
205             if (newParamValue < 1)
206                 _mousePosOnLast1Cross = -float.infinity;
207 
208             if (newParamValue != oldParamValue)
209             {
210                 if (auto p = cast(FloatParameter)_param)
211                 {
212                     p.setFromGUINormalized(newParamValue);
213                 }
214                 /*else if (auto p = cast(IntegerParameter)_param)
215                 {
216                     p.setFromGUINormalized(newParamValue);
217                     _draggingDebt = newParamValue - p.getNormalized();
218                 }*/
219                 else
220                     assert(false); // only integer and float parameters supported
221             }
222             setDirtyWhole();
223         }
224     }
225 
226     // For lazy updates
227     override void onBeginDrag()
228     {
229         _param.beginParamEdit();
230         setDirtyWhole();
231     }
232 
233     override void onStopDrag()
234     {
235         _param.endParamEdit();
236         setDirtyWhole();
237         _draggingDebt = 0.0f;
238     }
239 
240     override void onMouseEnter()
241     {
242         _param.beginParamHover();
243     }
244 
245     override void onMouseExit()
246     {
247         _param.endParamHover();
248     }
249 
250     override void onParameterChanged(Parameter sender)
251     {
252         setDirtyWhole();
253     }
254 
255     override void onBeginParameterEdit(Parameter sender)
256     {
257     }
258 
259     override void onEndParameterEdit(Parameter sender)
260     {
261     }
262 
263     override void onBeginParameterHover(Parameter sender)
264     {
265     }
266 
267     override void onEndParameterHover(Parameter sender)
268     {
269     }
270 
271     void disable()
272     {
273         _disabled = true;
274     }
275 
276 protected:
277 
278     /// The parameter this switch is linked with.
279     Parameter _param;
280 
281     /// Original slider image.
282     OwnedImage!RGBA _filmstrip;
283 
284     /// Resized slider image, full. Each frame image is resized independently and lazily.
285     OwnedImage!RGBA _filmstripResized;
286 
287     /// Which frames in the cache are resized.
288     bool[] _frameNthResized;
289 
290     /// The number of slider image frames contained in the _filmstrip image.
291     int _numFrames;
292 
293     /// The pixel height of slider frames in _filmstrip image.
294     /// _frameHeightOrig x _numFrames is the useful range of pixels, excess ones aren't used, if any.
295     int _frameHeightOrig;
296 
297     /// Sensivity: given a mouse movement in 100th of the height of the knob,
298     /// how much should the normalized parameter change.
299     float _sensivity;
300 
301 
302     float _mousePosOnLast0Cross;
303     float _mousePosOnLast1Cross;
304 
305     // Exists because small mouse drags for integer parameters may not
306     // lead to a parameter value change, hence a need to accumulate those drags.
307     float _draggingDebt = 0.0f;
308 
309     bool _disabled;
310 
311     ImageResizer _resizer;
312 
313     void clearCrosspoints()
314     {
315         _mousePosOnLast0Cross = float.infinity;
316         _mousePosOnLast1Cross = -float.infinity;
317     }
318 
319     void cacheImage(int frame)
320     {
321         if (_frameNthResized[frame]) 
322             return;
323 
324         int hdest   = _filmstripResized.h / _numFrames;
325         assert(hdest == position.height);
326 
327         //     context.globalImageResizer.resizeImage_sRGBWithAlpha(originalInput, _filmstripResized.toRef);
328         box2i origRect = rectangle(0, _frameHeightOrig * frame,       _filmstrip.w, _frameHeightOrig);
329         box2i destRect = rectangle(0,            hdest * frame, _filmstripResized.w,            hdest);
330 
331         // Note: in order to avoid slight sample offsets, it's even better that way, as each is resized separately.
332         ImageRef!RGBA source     = _filmstrip.toRef.cropImageRef(origRect);
333         ImageRef!RGBA dest       = _filmstripResized.toRef.cropImageRef(destRect);
334         _resizer.resizeImage_sRGBWithAlpha(source, dest);
335 
336         _frameNthResized[frame] = true;
337 
338     }
339 }