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 import std.algorithm.comparison; 12 13 import dplug.core.math; 14 15 import dplug.gui.element; 16 17 import dplug.client.params; 18 19 /** 20 * UIFilmstripKnob is a knob with a vertical RGBA image describing the knob graphics. 21 */ 22 class UIFilmstripKnob : UIElement, IParameterListener 23 { 24 public: 25 nothrow: 26 @nogc: 27 28 // This will change to 1.0f at one point for consistency, so better express your knob 29 // sensivity with that. 30 enum defaultSensivity = 0.25f; 31 32 this(UIContext context, Parameter param, OwnedImage!RGBA mipmap, int numFrames, float sensitivity = 0.25) 33 { 34 super(context, flagRaw); 35 _param = param; 36 _sensitivity = sensitivity; 37 _filmstrip = mipmap; 38 _numFrames = numFrames; 39 _knobWidth = _filmstrip.w; 40 _knobHeight = _filmstrip.h / _numFrames; 41 _param.addListener(this); 42 _disabled = false; 43 _filmstripScaled = mallocNew!(OwnedImage!RGBA)(); 44 _frameNthResized.reallocBuffer(_numFrames); 45 } 46 47 ~this() 48 { 49 _param.removeListener(this); 50 _filmstripScaled.destroyFree(); 51 _frameNthResized.reallocBuffer(0); 52 } 53 54 override void reflow() 55 { 56 int W = position.width; 57 int H = position.height * _numFrames; 58 if (_filmstripScaled.w != W || _filmstripScaled.h != H) 59 { 60 _filmstripScaled.size(position.width, position.height * _numFrames); 61 _frameNthResized[] = false; 62 } 63 } 64 65 /// Returns: sensivity. 66 float sensivity() 67 { 68 return _sensitivity; 69 } 70 71 /// Sets sensivity. 72 float sensivity(float sensitivity) 73 { 74 return _sensitivity = sensitivity; 75 } 76 77 void disable() 78 { 79 _disabled = true; 80 } 81 82 override void onDrawRaw(ImageRef!RGBA rawMap, box2i[] dirtyRects) 83 { 84 setCurrentImage(); 85 ImageRef!RGBA _currentImage = _filmstripScaled.toRef().cropImageRef(box2i(_imageX1, _imageY1, _imageX2, _imageY2)); 86 foreach(dirtyRect; dirtyRects) 87 { 88 ImageRef!RGBA croppedRawIn = _currentImage.cropImageRef(dirtyRect); 89 ImageRef!RGBA croppedRawOut = rawMap.cropImageRef(dirtyRect); 90 91 int w = dirtyRect.width; 92 int h = dirtyRect.height; 93 94 for(int j = 0; j < h; ++j) 95 { 96 RGBA[] input = croppedRawIn.scanline(j); 97 RGBA[] output = croppedRawOut.scanline(j); 98 99 for(int i = 0; i < w; ++i) 100 { 101 ubyte alpha = input[i].a; 102 103 RGBA color = blendColor(input[i], output[i], alpha); 104 output[i] = color; 105 } 106 } 107 108 } 109 } 110 111 float distance(float x1, float x2, float y1, float y2) 112 { 113 return sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)); 114 } 115 116 void setCurrentImage() 117 { 118 float value = _param.getNormalized(); 119 currentFrame = cast(int)(round(value * (_numFrames - 1))); 120 121 if(currentFrame < 0) currentFrame = 0; 122 123 _imageX1 = 0; 124 _imageY1 = (_filmstripScaled.h / _numFrames) * currentFrame; 125 126 _imageX2 = _filmstripScaled.w; 127 _imageY2 = _imageY1 + (_filmstripScaled.h / _numFrames); 128 129 cacheImage(currentFrame); 130 } 131 132 override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 133 { 134 if (!containsPoint(x, y)) 135 return Click.unhandled; 136 137 // double-click => set to default 138 if (isDoubleClick || mstate.altPressed) 139 { 140 _param.beginParamEdit(); 141 if (auto fp = cast(FloatParameter)_param) 142 fp.setFromGUI(fp.defaultValue()); 143 else if (auto ip = cast(IntegerParameter)_param) 144 ip.setFromGUI(ip.defaultValue()); 145 else 146 assert(false); 147 _param.endParamEdit(); 148 } 149 150 _normalizedValueWhileDragging = _param.getNormalized(); 151 152 return Click.startDrag; // to initiate dragging 153 } 154 155 // Called when mouse drag this Element. 156 override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate) 157 { 158 if(!_disabled) 159 { 160 float displacementInHeight = cast(float)(dy) / _position.height; 161 162 float modifier = 1.0f; 163 if (mstate.shiftPressed || mstate.ctrlPressed) 164 modifier *= 0.1f; 165 166 double oldParamValue = _normalizedValueWhileDragging; 167 double newParamValue = oldParamValue - displacementInHeight * modifier * _sensitivity; 168 if (mstate.altPressed) 169 newParamValue = _param.getNormalizedDefault(); 170 171 if (y > _mousePosOnLast0Cross) 172 return; 173 if (y < _mousePosOnLast1Cross) 174 return; 175 176 if (newParamValue <= 0 && oldParamValue > 0) 177 _mousePosOnLast0Cross = y; 178 179 if (newParamValue >= 1 && oldParamValue < 1) 180 _mousePosOnLast1Cross = y; 181 182 if (newParamValue < 0) 183 newParamValue = 0; 184 if (newParamValue > 1) 185 newParamValue = 1; 186 187 if (newParamValue > 0) 188 _mousePosOnLast0Cross = float.infinity; 189 190 if (newParamValue < 1) 191 _mousePosOnLast1Cross = -float.infinity; 192 193 if (newParamValue != oldParamValue) 194 { 195 if (auto fp = cast(FloatParameter)_param) 196 fp.setFromGUINormalized(newParamValue); 197 else if (auto ip = cast(IntegerParameter)_param) 198 ip.setFromGUINormalized(newParamValue); 199 else 200 assert(false); 201 _normalizedValueWhileDragging = newParamValue; 202 } 203 } 204 } 205 206 // For lazy updates 207 override void onBeginDrag() 208 { 209 _param.beginParamEdit(); 210 setDirtyWhole(); 211 } 212 213 override void onStopDrag() 214 { 215 _param.endParamEdit(); 216 clearCrosspoints(); 217 setDirtyWhole(); 218 } 219 220 override void onMouseMove(int x, int y, int dx, int dy, MouseState mstate) 221 { 222 } 223 224 override void onMouseEnter() 225 { 226 _param.beginParamHover(); 227 } 228 229 override void onMouseExit() 230 { 231 _param.endParamHover(); 232 } 233 234 override void onParameterChanged(Parameter sender) nothrow @nogc 235 { 236 setDirtyWhole(); 237 } 238 239 override void onBeginParameterEdit(Parameter sender) 240 { 241 } 242 243 override void onEndParameterEdit(Parameter sender) 244 { 245 } 246 247 override void onBeginParameterHover(Parameter sender) 248 { 249 } 250 251 override void onEndParameterHover(Parameter sender) 252 { 253 } 254 255 protected: 256 257 /// The parameter this knob is linked with. 258 Parameter _param; 259 260 OwnedImage!RGBA _filmstrip; 261 OwnedImage!RGBA _filmstripScaled; 262 263 ImageRef!RGBA _currentImage; 264 265 int _numFrames; 266 int _imageX1, _imageX2, _imageY1, _imageY2; 267 int currentFrame; 268 269 int _knobWidth; 270 int _knobHeight; 271 272 float _pushedAnimation; 273 274 /// Sensivity: given a mouse movement in 100th of the height of the knob, 275 /// how much should the normalized parameter change. 276 float _sensitivity; 277 278 float _mousePosOnLast0Cross; 279 float _mousePosOnLast1Cross; 280 281 // Normalized value last set while dragging. Necessary for integer paramerers 282 // that may round that normalized value when setting the parameter. 283 float _normalizedValueWhileDragging; 284 285 bool _disabled; 286 287 ImageResizer _resizer; 288 bool[] _frameNthResized; 289 290 void clearCrosspoints() nothrow @nogc 291 { 292 _mousePosOnLast0Cross = float.infinity; 293 _mousePosOnLast1Cross = -float.infinity; 294 } 295 296 final bool containsPoint(int x, int y) nothrow @nogc 297 { 298 vec2f center = getCenter(); 299 return vec2f(x, y).distanceTo(center) < getRadius(); 300 } 301 302 /// Returns: largest square centered in _position 303 final box2i getSubsquare() pure const nothrow @nogc 304 { 305 // We'll draw entirely in the largest centered square in _position. 306 box2i subSquare; 307 if (_position.width > _position.height) 308 { 309 int offset = (_position.width - _position.height) / 2; 310 int minX = offset; 311 subSquare = box2i(minX, 0, minX + _position.height, _position.height); 312 } 313 else 314 { 315 int offset = (_position.height - _position.width) / 2; 316 int minY = offset; 317 subSquare = box2i(0, minY, _position.width, minY + _position.width); 318 } 319 return subSquare; 320 } 321 322 final float getRadius() pure const nothrow @nogc 323 { 324 return getSubsquare().width * 0.5f; 325 } 326 327 final vec2f getCenter() pure const nothrow @nogc 328 { 329 box2i subSquare = getSubsquare(); 330 float centerx = (subSquare.min.x + subSquare.max.x - 1) * 0.5f; 331 float centery = (subSquare.min.y + subSquare.max.y - 1) * 0.5f; 332 return vec2f(centerx, centery); 333 } 334 335 // Resize sub-image lazily. Instead of doing it in `reflow()` for all frames, 336 // we do it in a case by case basis when needed in `onDrawRaw`. 337 void cacheImage(int frame) 338 { 339 // Limitation: the source image should have identically sized sub-frames. 340 assert(_filmstrip.h % _numFrames == 0); 341 342 if (_frameNthResized[frame]) 343 return; 344 345 int hsource = _filmstrip.h / _numFrames; 346 int hdest = _filmstripScaled.h / _numFrames; 347 348 // Note: in order to avoid slight sample offsets, each subframe needs to be resized separately. 349 ImageRef!RGBA source = _filmstrip.toRef.cropImageRef(rectangle(0, hsource * frame, _filmstrip.w, hsource)); 350 ImageRef!RGBA dest = _filmstripScaled.toRef.cropImageRef(rectangle(0, hdest * frame, _filmstripScaled.w, hdest )); 351 _resizer.resizeImage_sRGBNoAlpha(source, dest); 352 353 _frameNthResized[frame] = true; 354 } 355 }