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 }