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 }