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