1 /** 2 On/Off switch. 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.onoffswitch; 9 10 import std.math: exp, abs; 11 import dplug.core.math; 12 import dplug.gui.element; 13 import dplug.gui.bufferedelement; 14 import dplug.client.params; 15 16 class UIOnOffSwitch : UIElement, IParameterListener 17 { 18 public: 19 nothrow: 20 @nogc: 21 22 enum Orientation 23 { 24 vertical, 25 horizontal 26 } 27 @ScriptProperty RGBA diffuseOff = RGBA(230, 80, 43, 0); 28 @ScriptProperty RGBA diffuseOn = RGBA(230, 80, 43, 200); 29 @ScriptProperty RGBA material = RGBA(192, 10, 128, 255); 30 @ScriptProperty float animationTimeConstant = 10.0f; 31 @ScriptProperty ushort depthLow = 0; 32 @ScriptProperty ushort depthHigh = 30000; 33 @ScriptProperty ushort holeDepth = 0; 34 @ScriptProperty Orientation orientation = Orientation.vertical; 35 @ScriptProperty bool drawDepth = true; 36 @ScriptProperty bool drawDiffuse = true; 37 @ScriptProperty bool drawMaterial = true; 38 @ScriptProperty bool drawHole = true; // if drawDepth && drawHole, draw Z hole 39 @ScriptProperty bool drawEmissive = true; // if drawEmissive && !drawDiffuse, draw just the emissive channel 40 41 42 /// Left and right border, in fraction of the widget's width. 43 /// Cannot be > 0.5f 44 @ScriptProperty float borderHorz = 0.1f; 45 46 /// Top and bottom border, in fraction of the widget's width.* 47 /// Cannot be > 0.5f 48 @ScriptProperty float borderVert = 0.1f; 49 50 /// Note: Can take a BoolParameter or IntegerParameter, in which 51 /// case enabled means 1 and disabled means 0. 52 /// So it also takes `EnumParameter`. 53 this(UIContext context, Parameter param) 54 { 55 super(context, flagAnimated | flagPBR); 56 _param = param; 57 _param.addListener(this); 58 _animation = 0.0f; 59 } 60 61 ~this() 62 { 63 _param.removeListener(this); 64 } 65 66 override void onAnimate(double dt, double time) nothrow @nogc 67 { 68 float target; 69 if (auto bp = cast(BoolParameter)_param) 70 { 71 target = bp.valueAtomic() ? 1 : 0; 72 } 73 else if (auto ip = cast(IntegerParameter)_param) 74 { 75 target = (ip.valueAtomic() != 0) ? 1 : 0; 76 } 77 else 78 assert(false); 79 80 float newAnimation = lerp(_animation, target, 1.0 - exp(-dt * animationTimeConstant)); 81 82 if (abs(newAnimation - _animation) > 0.001f) 83 { 84 _animation = newAnimation; 85 setDirtyWhole(); 86 } 87 } 88 89 override void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc 90 { 91 // The switch is in a subrect 92 int width = _position.width; 93 int height = _position.height; 94 95 box2i switchRect = box2i( cast(int)(0.5f + width * borderHorz), 96 cast(int)(0.5f + height * borderVert), 97 cast(int)(0.5f + width * (1 - borderHorz)), 98 cast(int)(0.5f + height * (1 - borderVert)) ); 99 100 ubyte red = cast(ubyte)(lerp!float(diffuseOff.r, diffuseOn.r, _animation)); 101 ubyte green = cast(ubyte)(lerp!float(diffuseOff.g, diffuseOn.g, _animation)); 102 ubyte blue = cast(ubyte)(lerp!float(diffuseOff.b, diffuseOn.b, _animation)); 103 int emissive = cast(ubyte)(lerp!float(diffuseOff.a, diffuseOn.a, _animation)); 104 105 if (isMouseOver || isDragged) 106 emissive += 40; 107 108 if (emissive > 255) 109 emissive = 255; 110 111 // Workaround issue https://issues.dlang.org/show_bug.cgi?id=23076 112 // Regular should not be inlined here. 113 static float lerpfloat(float a, float b, float t) pure nothrow @nogc 114 { 115 pragma(inline, false); 116 return t * b + (1 - t) * a; 117 } 118 119 L16 depthA = L16(cast(short)(lerpfloat(depthHigh, depthLow, _animation))); 120 L16 depthB = L16(cast(short)(lerpfloat(depthLow, depthHigh, _animation))); 121 122 RGBA diffuseColor = RGBA(red, green, blue, cast(ubyte)emissive); 123 124 foreach(dirtyRect; dirtyRects) 125 { 126 auto cDepth = depthMap.cropImageRef(dirtyRect); 127 128 // Write a plain color in the diffuse and material map. 129 box2i validRect = dirtyRect.intersection(switchRect); 130 if (!validRect.empty) 131 { 132 ImageRef!RGBA cDiffuse = diffuseMap.cropImageRef(validRect); 133 if (drawDiffuse) 134 { 135 cDiffuse.fillAll(diffuseColor); 136 } 137 else if (drawEmissive) 138 { 139 cDiffuse.fillRectAlpha(0, 0, cDiffuse.w, cDiffuse.h, diffuseColor.a); 140 } 141 if (drawMaterial) 142 materialMap.cropImageRef(validRect).fillAll(material); 143 } 144 145 // dig a hole 146 if (drawDepth) 147 { 148 if (drawHole) 149 cDepth.fillAll(L16(holeDepth)); 150 151 box2i slopeRect = switchRect.translate(-dirtyRect.min.x, -dirtyRect.min.y); 152 153 if (orientation == Orientation.vertical) 154 verticalSlope(cDepth, slopeRect, depthA, depthB); 155 else 156 horizontalSlope(cDepth, slopeRect, depthA, depthB); 157 } 158 } 159 } 160 161 override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 162 { 163 if (!_canBeDragged) 164 { 165 // inside gesture, refuse new clicks that could call 166 // excess beginParamEdit()/endParamEdit() 167 return Click.unhandled; 168 } 169 170 // ALT + click => set it to default 171 if (mstate.altPressed) // reset on ALT + click 172 { 173 _param.beginParamEdit(); 174 175 if (auto bp = cast(BoolParameter)_param) 176 { 177 bp.setFromGUI(bp.defaultValue()); 178 } 179 else if (auto ip = cast(IntegerParameter)_param) 180 { 181 ip.setFromGUI(ip.defaultValue()); // possibly not 0 and 1 182 } 183 else 184 assert(false); 185 } 186 else 187 { 188 // Any click => invert 189 // Note: double-click doesn't reset to default, would be annoying 190 _param.beginParamEdit(); 191 if (auto bp = cast(BoolParameter)_param) 192 { 193 bp.setFromGUI(!bp.value()); 194 } 195 else if (auto ip = cast(IntegerParameter)_param) 196 { 197 // any => 0 198 // 0 => 1 199 bool value = ip.value() != 0; 200 ip.setFromGUI(value ? 0 : 1); 201 } 202 else 203 assert(false); 204 } 205 _canBeDragged = false; 206 return Click.startDrag; 207 } 208 209 override void onMouseEnter() 210 { 211 _param.beginParamHover(); 212 setDirtyWhole(); 213 } 214 215 override void onMouseExit() 216 { 217 _param.endParamHover(); 218 setDirtyWhole(); 219 } 220 221 override void onStopDrag() 222 { 223 // End parameter edit at end of dragging, even if no parameter change happen, 224 // so that touch automation restore previous parameter value at the end of the mouse 225 // gesture. 226 _param.endParamEdit(); 227 _canBeDragged = true; 228 } 229 230 override void onParameterChanged(Parameter sender) nothrow @nogc 231 { 232 setDirtyWhole(); 233 } 234 235 override void onBeginParameterEdit(Parameter sender) 236 { 237 } 238 239 override void onEndParameterEdit(Parameter sender) 240 { 241 } 242 243 override void onBeginParameterHover(Parameter sender) 244 { 245 } 246 247 override void onEndParameterHover(Parameter sender) 248 { 249 } 250 251 protected: 252 253 /// The parameter this switch is linked with. 254 Parameter _param; 255 256 /// To prevent multiple-clicks having an adverse effect on automation. 257 bool _canBeDragged = true; 258 259 private: 260 float _animation; 261 } 262 263 private: 264 void fillRectAlpha(bool CHECKED=true, V)(auto ref V v, int x1, int y1, int x2, int y2, ubyte alpha) nothrow @nogc 265 if (isWritableView!V && is(RGBA : ViewColor!V)) 266 { 267 sort2(x1, x2); 268 sort2(y1, y2); 269 static if (CHECKED) 270 { 271 if (x1 >= v.w || y1 >= v.h || x2 <= 0 || y2 <= 0 || x1==x2 || y1==y2) return; 272 if (x1 < 0) x1 = 0; 273 if (y1 < 0) y1 = 0; 274 if (x2 >= v.w) x2 = v.w; 275 if (y2 >= v.h) y2 = v.h; 276 } 277 278 foreach (y; y1..y2) 279 { 280 RGBA[] scan = v.scanline(y); 281 foreach (x; x1..x2) 282 { 283 scan[x].a = alpha; 284 } 285 } 286 }