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