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 }