1 /** 2 Slider. 3 4 Copyright: Copyright Auburn Sounds 2015-2017. 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 Authors: Guillaume Piolat 7 */ 8 module dplug.pbrwidgets.slider; 9 10 import std.math: exp, abs; 11 12 import dplug.core.math; 13 import dplug.gui.bufferedelement; 14 import dplug.client.params; 15 16 17 enum HandleStyle 18 { 19 shapeW, 20 shapeV, 21 shapeA, 22 shapeBlock 23 } 24 25 class UISlider : UIBufferedElementPBR, IParameterListener 26 { 27 public: 28 nothrow: 29 @nogc: 30 31 // Trail customization 32 @ScriptProperty L16 trailDepth = L16(30000); 33 @ScriptProperty RGBA unlitTrailDiffuse = RGBA(130, 90, 45, 5); 34 @ScriptProperty RGBA litTrailDiffuse = RGBA(240, 165, 102, 130); 35 @ScriptProperty float trailWidth = 0.2f; 36 37 @ScriptProperty RGBA litTrailDiffuseAlt = RGBA(240, 165, 102, 130); 38 @ScriptProperty bool hasAlternateTrail = false; 39 @ScriptProperty float trailBase = 0.0f; // trail is from trailBase to parameter value 40 41 // Handle customization 42 @ScriptProperty HandleStyle handleStyle = HandleStyle.shapeW; 43 @ScriptProperty float handleHeightRatio = 0.25f; 44 @ScriptProperty float handleWidthRatio = 0.7f; 45 @ScriptProperty RGBA handleDiffuse = RGBA(248, 245, 233, 16); 46 @ScriptProperty RGBA handleMaterial = RGBA(0, 255, 128, 255); 47 48 this(UIContext context, Parameter param) 49 { 50 super(context, flagAnimated | flagPBR); 51 _param = param; 52 _param.addListener(this); 53 _sensivity = 1.0f; 54 _pushedAnimation = 0; 55 clearCrosspoints(); 56 setCursorWhenDragged(MouseCursor.drag); 57 setCursorWhenMouseOver(MouseCursor.move); 58 } 59 60 ~this() 61 { 62 _param.removeListener(this); 63 } 64 65 /// Returns: sensivity. 66 float sensivity() 67 { 68 return _sensivity; 69 } 70 71 /// Sets sensivity. 72 float sensivity(float sensivity) 73 { 74 return _sensivity = sensivity; 75 } 76 77 override void onAnimate(double dt, double time) nothrow @nogc 78 { 79 float target = isDragged() ? 1 : 0; 80 float newAnimation = lerp(_pushedAnimation, target, 1.0 - exp(-dt * 30)); 81 if (abs(newAnimation - _pushedAnimation) > 0.001f) 82 { 83 _pushedAnimation = newAnimation; 84 setDirtyWhole(); 85 } 86 } 87 88 override void onDrawBufferedPBR(ImageRef!RGBA diffuseMap, 89 ImageRef!L16 depthMap, 90 ImageRef!RGBA materialMap, 91 ImageRef!L8 diffuseOpacity, 92 ImageRef!L8 depthOpacity, 93 ImageRef!L8 materialOpacity) nothrow @nogc 94 { 95 int width = _position.width; 96 int height = _position.height; 97 int handleHeight = cast(int)(0.5f + this.handleHeightRatio * height * (1 - 0.12f * _pushedAnimation)); 98 int handleWidth = cast(int)(0.5f + this.handleWidthRatio * width * (1 - 0.06f * _pushedAnimation)); 99 int trailWidth = cast(int)(0.5f + width * this.trailWidth); 100 101 int handleHeightUnpushed = cast(int)(0.5f + this.handleHeightRatio * height); 102 int trailMargin = cast(int)(0.5f + (handleHeightUnpushed - trailWidth) * 0.5f); 103 if (trailMargin < 0) 104 trailMargin = 0; 105 106 int trailX = cast(int)(0.5 + (width - trailWidth) * 0.5f); 107 int trailHeight = height - 2 * trailMargin; 108 109 // The switch is in a subrect 110 111 box2i holeRect = box2i(trailX, trailMargin, trailX + trailWidth, trailMargin + trailHeight); 112 113 float value = _param.getNormalized(); 114 115 int posX = cast(int)(0.5f + (width - handleWidth) * 0.5f); 116 int posY = cast(int)(0.5f + (1 - value) * (height - handleHeight)); 117 assert(posX >= 0); 118 assert(posY >= 0); 119 120 box2i handleRect = box2i(posX, posY, posX + handleWidth, posY + handleHeight); 121 122 123 // Dig hole and paint trail deeper hole 124 { 125 126 127 depthMap.cropImageRef(holeRect).fillAll(trailDepth); 128 129 // Fill opacity for hole 130 diffuseOpacity.cropImageRef(holeRect).fillAll(opacityFullyOpaque); 131 depthOpacity.cropImageRef(holeRect).fillAll(opacityFullyOpaque); 132 133 int valueToTrail(float value) nothrow @nogc 134 { 135 return cast(int)(0.5f + (1 - value) * (height+4 - handleHeight) + handleHeight*0.5f - 2); 136 } 137 138 void paintTrail(float from, float to, RGBA diffuse) nothrow @nogc 139 { 140 int ymin = valueToTrail(from); 141 int ymax = valueToTrail(to); 142 if (ymin > ymax) 143 { 144 int temp = ymin; 145 ymin = ymax; 146 ymax = temp; 147 } 148 box2i b = box2i(holeRect.min.x, ymin, holeRect.max.x, ymax); 149 diffuseMap.cropImageRef(b).fillAll(diffuse); 150 } 151 152 153 154 RGBA litTrail = (value >= trailBase) ? litTrailDiffuse : litTrailDiffuseAlt; 155 if (isDragged) 156 { 157 // lit trail is 50% brighter when dragged 158 int alpha = 3 * litTrail.a / 2; 159 if (alpha > 255) 160 alpha = 255; 161 litTrail.a = cast(ubyte) alpha; 162 } 163 164 paintTrail(0, 1, unlitTrailDiffuse); 165 paintTrail(trailBase, value, litTrail); 166 } 167 168 // Paint handle of slider 169 int emissive = handleDiffuse.a; 170 if (isMouseOver && !isDragged) 171 emissive += 50; 172 if (emissive > 255) 173 emissive = 255; 174 175 RGBA handleDiffuseLit = RGBA(handleDiffuse.r, handleDiffuse.g, handleDiffuse.b, cast(ubyte)emissive); 176 177 diffuseMap.cropImageRef(handleRect).fillAll(handleDiffuseLit); 178 179 if (handleStyle == HandleStyle.shapeV) 180 { 181 auto c0 = L16(20000); 182 auto c1 = L16(50000); 183 184 int h0 = handleRect.min.y; 185 int h1 = handleRect.center.y; 186 int h2 = handleRect.max.y; 187 188 verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1); 189 verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0); 190 } 191 else if (handleStyle == HandleStyle.shapeA) 192 { 193 auto c0 = L16(50000); 194 auto c1 = L16(20000); 195 196 int h0 = handleRect.min.y; 197 int h1 = handleRect.center.y; 198 int h2 = handleRect.max.y; 199 200 verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1); 201 verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0); 202 } 203 else if (handleStyle == HandleStyle.shapeW) 204 { 205 auto c0 = L16(15000); 206 auto c1 = L16(65535); 207 auto c2 = L16(51400); 208 209 int h0 = handleRect.min.y; 210 int h1 = (handleRect.min.y * 3 + handleRect.max.y + 2) / 4; 211 int h2 = handleRect.center.y; 212 int h3 = (handleRect.min.y + handleRect.max.y * 3 + 2) / 4; 213 int h4 = handleRect.max.y; 214 215 verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1); 216 verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c2); 217 verticalSlope(depthMap, box2i(handleRect.min.x, h2, handleRect.max.x, h3), c2, c1); 218 verticalSlope(depthMap, box2i(handleRect.min.x, h3, handleRect.max.x, h4), c1, c0); 219 } 220 else if (handleStyle == HandleStyle.shapeBlock) 221 { 222 depthMap.cropImageRef(handleRect).fillAll(L16(50000)); 223 } 224 225 materialMap.cropImageRef(handleRect).fillAll(handleMaterial); 226 227 // Fill opacity for handle 228 diffuseOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque); 229 depthOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque); 230 materialOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque); 231 } 232 233 override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 234 { 235 // double-click => set to default 236 if (isDoubleClick || mstate.altPressed) 237 { 238 if (auto p = cast(FloatParameter)_param) 239 { 240 p.beginParamEdit(); 241 p.setFromGUI(p.defaultValue()); 242 p.endParamEdit(); 243 } 244 else if (auto p = cast(IntegerParameter)_param) 245 { 246 p.beginParamEdit(); 247 p.setFromGUI(p.defaultValue()); 248 p.endParamEdit(); 249 } 250 else 251 assert(false); // only integer and float parameters supported 252 } 253 254 return Click.startDrag; // to initiate dragging 255 } 256 257 // Called when mouse drag this Element. 258 override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate) 259 { 260 // FUTURE: replace by actual trail height instead of total height 261 float displacementInHeight = cast(float)(dy) / _position.height; 262 263 float modifier = 1.0f; 264 if (mstate.shiftPressed || mstate.ctrlPressed) 265 modifier *= 0.1f; 266 267 double oldParamValue = _param.getNormalized() + _draggingDebt; 268 double newParamValue = oldParamValue - displacementInHeight * modifier * _sensivity; 269 if (mstate.altPressed) 270 newParamValue = _param.getNormalizedDefault(); 271 272 if (y > _mousePosOnLast0Cross) 273 return; 274 if (y < _mousePosOnLast1Cross) 275 return; 276 277 if (newParamValue <= 0 && oldParamValue > 0) 278 _mousePosOnLast0Cross = y; 279 280 if (newParamValue >= 1 && oldParamValue < 1) 281 _mousePosOnLast1Cross = y; 282 283 if (newParamValue < 0) 284 newParamValue = 0; 285 if (newParamValue > 1) 286 newParamValue = 1; 287 288 if (newParamValue > 0) 289 _mousePosOnLast0Cross = float.infinity; 290 291 if (newParamValue < 1) 292 _mousePosOnLast1Cross = -float.infinity; 293 294 if (newParamValue != oldParamValue) 295 { 296 if (auto p = cast(FloatParameter)_param) 297 { 298 p.setFromGUINormalized(newParamValue); 299 } 300 else if (auto p = cast(IntegerParameter)_param) 301 { 302 p.setFromGUINormalized(newParamValue); 303 _draggingDebt = newParamValue - p.getNormalized(); 304 } 305 else 306 assert(false); // only integer and float parameters supported 307 } 308 } 309 310 // For lazy updates 311 override void onBeginDrag() 312 { 313 _param.beginParamEdit(); 314 setDirtyWhole(); 315 } 316 317 override void onStopDrag() 318 { 319 _param.endParamEdit(); 320 setDirtyWhole(); 321 _draggingDebt = 0.0f; 322 } 323 324 override void onMouseEnter() 325 { 326 _param.beginParamHover(); 327 setDirtyWhole(); 328 } 329 330 override void onMouseExit() 331 { 332 _param.endParamHover(); 333 setDirtyWhole(); 334 } 335 336 override void onParameterChanged(Parameter sender) 337 { 338 setDirtyWhole(); 339 } 340 341 override void onBeginParameterEdit(Parameter sender) 342 { 343 } 344 345 override void onEndParameterEdit(Parameter sender) 346 { 347 } 348 349 override void onBeginParameterHover(Parameter sender) 350 { 351 } 352 353 override void onEndParameterHover(Parameter sender) 354 { 355 } 356 357 358 protected: 359 360 /// The parameter this switch is linked with. 361 Parameter _param; 362 363 /// Sensivity: given a mouse movement in 100th of the height of the knob, 364 /// how much should the normalized parameter change. 365 float _sensivity; 366 367 float _pushedAnimation; 368 369 float _mousePosOnLast0Cross; 370 float _mousePosOnLast1Cross; 371 372 // Exists because small mouse drags for integer parameters may not 373 // lead to a parameter value change, hence a need to accumulate those drags. 374 float _draggingDebt = 0.0f; 375 376 void clearCrosspoints() 377 { 378 _mousePosOnLast0Cross = float.infinity; 379 _mousePosOnLast1Cross = -float.infinity; 380 } 381 }