1 /** 2 * PBR widget: sliders. 3 * 4 * Copyright: Copyright Auburn Sounds 2015 and later. 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; 11 import std.algorithm.comparison; 12 13 import dplug.core.math; 14 import dplug.graphics.drawex; 15 import dplug.gui.bufferedelement; 16 import dplug.client.params; 17 18 19 enum HandleStyle 20 { 21 shapeW, 22 shapeV, 23 shapeA, 24 shapeBlock 25 } 26 27 class UISlider : UIBufferedElement, IParameterListener 28 { 29 public: 30 nothrow: 31 @nogc: 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 this(UIContext context, Parameter param) 51 { 52 super(context); 53 _param = param; 54 _param.addListener(this); 55 _sensivity = 1.0f; 56 _pushedAnimation = 0; 57 clearCrosspoints(); 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 onDrawBuffered(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 litTrail.a = cast(ubyte) std.algorithm.min(255, 3 * litTrail.a / 2); 159 } 160 161 paintTrail(0, 1, unlitTrailDiffuse); 162 paintTrail(trailBase, value, litTrail); 163 } 164 165 // Paint handle of slider 166 int emissive = handleDiffuse.a; 167 if (isMouseOver && !isDragged) 168 emissive += 50; 169 if (emissive > 255) 170 emissive = 255; 171 172 RGBA handleDiffuseLit = RGBA(handleDiffuse.r, handleDiffuse.g, handleDiffuse.b, cast(ubyte)emissive); 173 174 diffuseMap.cropImageRef(handleRect).fillAll(handleDiffuseLit); 175 176 if (handleStyle == HandleStyle.shapeV) 177 { 178 auto c0 = L16(20000); 179 auto c1 = L16(50000); 180 181 int h0 = handleRect.min.y; 182 int h1 = handleRect.center.y; 183 int h2 = handleRect.max.y; 184 185 verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1); 186 verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0); 187 } 188 else if (handleStyle == HandleStyle.shapeA) 189 { 190 auto c0 = L16(50000); 191 auto c1 = L16(20000); 192 193 int h0 = handleRect.min.y; 194 int h1 = handleRect.center.y; 195 int h2 = handleRect.max.y; 196 197 verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1); 198 verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c0); 199 } 200 else if (handleStyle == HandleStyle.shapeW) 201 { 202 auto c0 = L16(15000); 203 auto c1 = L16(65535); 204 auto c2 = L16(51400); 205 206 int h0 = handleRect.min.y; 207 int h1 = (handleRect.min.y * 3 + handleRect.max.y + 2) / 4; 208 int h2 = handleRect.center.y; 209 int h3 = (handleRect.min.y + handleRect.max.y * 3 + 2) / 4; 210 int h4 = handleRect.max.y; 211 212 verticalSlope(depthMap, box2i(handleRect.min.x, h0, handleRect.max.x, h1), c0, c1); 213 verticalSlope(depthMap, box2i(handleRect.min.x, h1, handleRect.max.x, h2), c1, c2); 214 verticalSlope(depthMap, box2i(handleRect.min.x, h2, handleRect.max.x, h3), c2, c1); 215 verticalSlope(depthMap, box2i(handleRect.min.x, h3, handleRect.max.x, h4), c1, c0); 216 } 217 else if (handleStyle == HandleStyle.shapeBlock) 218 { 219 depthMap.cropImageRef(handleRect).fillAll(L16(50000)); 220 } 221 222 materialMap.cropImageRef(handleRect).fillAll(handleMaterial); 223 224 // Fill opacity for handle 225 diffuseOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque); 226 depthOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque); 227 materialOpacity.cropImageRef(handleRect).fillAll(opacityFullyOpaque); 228 } 229 230 override bool onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 231 { 232 // double-click => set to default 233 if (isDoubleClick) 234 { 235 if (auto p = cast(FloatParameter)_param) 236 { 237 p.beginParamEdit(); 238 p.setFromGUI(p.defaultValue()); 239 p.endParamEdit(); 240 } 241 else if (auto p = cast(IntegerParameter)_param) 242 { 243 p.beginParamEdit(); 244 p.setFromGUI(p.defaultValue()); 245 p.endParamEdit(); 246 } 247 else 248 assert(false); // only integer and float parameters supported 249 } 250 251 return true; // to initiate dragging 252 } 253 254 // Called when mouse drag this Element. 255 override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate) 256 { 257 // FUTURE: replace by actual trail height instead of total height 258 float displacementInHeight = cast(float)(dy) / _position.height; 259 260 float modifier = 1.0f; 261 if (mstate.shiftPressed || mstate.ctrlPressed) 262 modifier *= 0.1f; 263 264 double oldParamValue = _param.getNormalized() + _draggingDebt; 265 double newParamValue = oldParamValue - displacementInHeight * modifier * _sensivity; 266 267 if (y > _mousePosOnLast0Cross) 268 return; 269 if (y < _mousePosOnLast1Cross) 270 return; 271 272 if (newParamValue <= 0 && oldParamValue > 0) 273 _mousePosOnLast0Cross = y; 274 275 if (newParamValue >= 1 && oldParamValue < 1) 276 _mousePosOnLast1Cross = y; 277 278 if (newParamValue < 0) 279 newParamValue = 0; 280 if (newParamValue > 1) 281 newParamValue = 1; 282 283 if (newParamValue > 0) 284 _mousePosOnLast0Cross = float.infinity; 285 286 if (newParamValue < 1) 287 _mousePosOnLast1Cross = -float.infinity; 288 289 if (newParamValue != oldParamValue) 290 { 291 if (auto p = cast(FloatParameter)_param) 292 { 293 p.setFromGUINormalized(newParamValue); 294 } 295 else if (auto p = cast(IntegerParameter)_param) 296 { 297 p.setFromGUINormalized(newParamValue); 298 _draggingDebt = newParamValue - p.getNormalized(); 299 } 300 else 301 assert(false); // only integer and float parameters supported 302 } 303 } 304 305 // For lazy updates 306 override void onBeginDrag() 307 { 308 _param.beginParamEdit(); 309 setDirtyWhole(); 310 } 311 312 override void onStopDrag() 313 { 314 _param.endParamEdit(); 315 setDirtyWhole(); 316 _draggingDebt = 0.0f; 317 } 318 319 override void onMouseEnter() 320 { 321 setDirtyWhole(); 322 } 323 324 override void onMouseExit() 325 { 326 setDirtyWhole(); 327 } 328 329 override void onParameterChanged(Parameter sender) 330 { 331 setDirtyWhole(); 332 } 333 334 override void onBeginParameterEdit(Parameter sender) 335 { 336 } 337 338 override void onEndParameterEdit(Parameter sender) 339 { 340 } 341 342 protected: 343 344 /// The parameter this switch is linked with. 345 Parameter _param; 346 347 /// Sensivity: given a mouse movement in 100th of the height of the knob, 348 /// how much should the normalized parameter change. 349 float _sensivity; 350 351 float _pushedAnimation; 352 353 float _mousePosOnLast0Cross; 354 float _mousePosOnLast1Cross; 355 356 // Exists because small mouse drags for integer parameters may not 357 // lead to a parameter value change, hence a need to accumulate those drags. 358 float _draggingDebt = 0.0f; 359 360 void clearCrosspoints() 361 { 362 _mousePosOnLast0Cross = float.infinity; 363 _mousePosOnLast1Cross = -float.infinity; 364 } 365 }