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