1 /** 2 Defines possible size for a plugin. 3 4 Copyright: Guillaume Piolat 2020. 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 * Authors: Guillaume Piolat 7 */ 8 module dplug.gui.sizeconstraints; 9 10 import dplug.core.math; 11 12 nothrow: 13 @nogc: 14 @safe: 15 16 17 /// Build a `SizeConstraints` that describes a fixed UI dimensions, in logical pixels. 18 SizeConstraints makeSizeConstraintsFixed(int width, int height) 19 { 20 float[1] ratio = 1.0f; 21 return makeSizeConstraintsDiscrete(width, height, ratio[]); 22 } 23 24 /// Build a `SizeConstraints` that describes multiple UI dimensions, in logical pixels. 25 /// Aspect ratio is preserved. 26 /// 27 /// Params: 28 /// defaultWidth base width in pixels. 29 /// defaultHeight base height in pixels. 30 /// availableRatios sorted list of UI scale factors, should contain 1.0f. 31 /// That list of scale factors: - must be increasing 32 /// - must contain 1.0f 33 /// - all factors must be > 0.0f 34 /// 35 /// Warning: no more than 8 possible scales are possible. 36 SizeConstraints makeSizeConstraintsDiscrete(int defaultWidth, 37 int defaultHeight, 38 scope const(float)[] availableScales) 39 { 40 SizeConstraints sc; 41 sc.type = SizeConstraints.Type.discreteRatio; 42 sc.defaultWidth = defaultWidth; 43 sc.defaultHeight = defaultHeight; 44 assert(availableScales.length <= SizeConstraints.MAX_POSSIBLE_SCALES); 45 int N = cast(int)availableScales.length; 46 sc.numDiscreteScales = N; 47 sc.discreteScales[0..N] = availableScales[0..N]; 48 return sc; 49 } 50 51 /// Build a `SizeConstraints` that describes UI dimensions, in logical pixels. 52 /// Aspect ratio is preserved over a range of continuously possible scale factors. 53 /// 54 /// Params: 55 /// defaultWidth base width in pixels. 56 /// defaultHeight base height in pixels. 57 /// availableRatios sorted list of ratios, should contain 1.0f. 58 SizeConstraints makeSizeConstraintsContinuous(int defaultWidth, 59 int defaultHeight, 60 float minScale, 61 float maxScale) 62 { 63 assert(minScale > 0.0f && minScale <= 1.0f); 64 assert(maxScale >= 1.0f); 65 SizeConstraints sc; 66 sc.type = SizeConstraints.Type.continuousRatio; 67 sc.defaultWidth = defaultWidth; 68 sc.defaultHeight = defaultHeight; 69 sc.minScale = minScale; 70 sc.maxScale = maxScale; 71 return sc; 72 } 73 74 /// Build a `SizeConstraints` that describes a rectangular range of size, in logical pixels. 75 /// All continous sizes are valid within these bounds. 76 /// Aspect ratio is NOT preserved. 77 SizeConstraints makeSizeConstraintsBounds(int minWidth, 78 int minHeight, 79 int maxWidth, 80 int maxHeight, 81 int defaultWidth, 82 int defaultHeight) 83 { 84 assert(defaultWidth >= minWidth && defaultWidth <= maxWidth); 85 assert(defaultHeight >= minHeight && defaultHeight <= maxHeight); 86 87 SizeConstraints sc; 88 sc.type = SizeConstraints.Type.rectangularBounds; 89 sc.defaultWidth = defaultWidth; 90 sc.defaultHeight = defaultHeight; 91 sc.minWidth = minWidth; 92 sc.maxWidth = maxWidth; 93 sc.minHeight = minHeight; 94 sc.maxHeight = maxHeight; 95 return sc; 96 } 97 98 99 /// Build a `SizeConstraints` that describes several scale factors for X and Y, in logical pixels. 100 /// Aspect ratio is NOT preserved. 101 /// 102 /// Params: 103 /// defaultWidth base width in pixels. 104 /// defaultHeight base height in pixels. 105 /// availableRatiosX sorted list of UI scale factors for the X dimension, should contain 1.0f. 106 /// That list of scale factors: - must be increasing 107 /// - must contain 1.0f 108 /// - all factors must be > 0.0f 109 /// availableRatiosY sorted list of UI scale factors for the Y dimension. Same as above. 110 /// 111 /// Warning: no more than 8 possible scales are possible for each axis. 112 SizeConstraints makeSizeConstraintsDiscreteXY(int defaultWidth, 113 int defaultHeight, 114 const(float)[] availableRatiosX, 115 const(float)[] availableRatiosY) 116 { 117 SizeConstraints sc; 118 sc.type = SizeConstraints.Type.discreteRatioXY; 119 sc.defaultWidth = defaultWidth; 120 sc.defaultHeight = defaultHeight; 121 assert(availableRatiosX.length <= SizeConstraints.MAX_POSSIBLE_SCALES); 122 assert(availableRatiosY.length <= SizeConstraints.MAX_POSSIBLE_SCALES); 123 124 int N = cast(int)availableRatiosX.length; 125 sc.numDiscreteScalesX = N; 126 sc.discreteScalesX[0..N] = availableRatiosX[0..N]; 127 128 N = cast(int)availableRatiosY.length; 129 sc.numDiscreteScalesY = N; 130 sc.discreteScalesY[0..N] = availableRatiosY[0..N]; 131 return sc; 132 } 133 134 135 /// Describe what size in logical pixels are possible. 136 /// A GUIGraphics is given a `SizeConstraints` in its constructor. 137 struct SizeConstraints 138 { 139 public: 140 nothrow: 141 @nogc: 142 143 enum Type /// Internal type of size constraint 144 { 145 continuousRatio, /// Continuous zoom factors, preserve aspect ratio 146 discreteRatio, /// Discrete zoom factors, preserve aspect ratio (recommended) 147 rectangularBounds, /// Continuous separate zoom factors for X and Y, given with rectangular bounds. 148 discreteRatioXY, /// Discrete separate zoom factors for X and Y (recommended) 149 } 150 151 /// Suggest a valid size for plugin first opening. 152 void suggestDefaultSize(int* width, int* height) 153 { 154 *width = defaultWidth; 155 *height = defaultHeight; 156 } 157 158 /// Returns `true` if several size are possible. 159 bool isResizable() 160 { 161 final switch(type) with (Type) 162 { 163 case continuousRatio: 164 return true; 165 case discreteRatio: 166 return numDiscreteScales > 1; 167 case rectangularBounds: 168 return true; 169 case discreteRatioXY: 170 return numDiscreteScalesX > 1 && numDiscreteScalesY > 1; 171 } 172 } 173 174 /// Returns `true` if this `SizeConstraints` preserve plugin aspect ratio. 175 bool preserveAspectRatio() 176 { 177 final switch(type) with (Type) 178 { 179 case continuousRatio: 180 case discreteRatio: 181 return true; 182 case rectangularBounds: 183 case discreteRatioXY: 184 return false; 185 } 186 } 187 188 /// Returns `true` if this `SizeConstraints` allows this size. 189 bool isValidSize(int width, int height) @trusted 190 { 191 int validw = width, 192 validh = height; 193 getNearestValidSize(&validw, &validh); 194 return validw == width && validh == height; // if the input size is valid, will return the same 195 } 196 197 /// Given an input size, get the nearest valid size. 198 void getNearestValidSize(int* inoutWidth, int* inoutHeight) 199 { 200 final switch(type) with (Type) 201 { 202 case continuousRatio: 203 // find estimate of scale 204 float scale = 0.5f * (*inoutWidth / (cast(float)defaultWidth) 205 + *inoutHeight / (cast(float)defaultHeight)); 206 if (scale < minScale) scale = minScale; 207 if (scale > maxScale) scale = maxScale; 208 *inoutWidth = cast(int)(0.5f + scale * defaultWidth); 209 *inoutHeight = cast(int)(0.5f + scale * defaultHeight); 210 break; 211 212 case discreteRatio: 213 { 214 float scale = 0.5f * (*inoutWidth / (cast(float)defaultWidth) 215 + *inoutHeight / (cast(float)defaultHeight)); 216 scale = findBestMatchingFloat(scale, discreteScales[0..numDiscreteScales]); 217 *inoutWidth = cast(int)(0.5f + scale * defaultWidth); 218 *inoutHeight = cast(int)(0.5f + scale * defaultHeight); 219 break; 220 } 221 222 case rectangularBounds: 223 alias w = inoutWidth; 224 alias h = inoutHeight; 225 if (*w < minWidth) *w = minWidth; 226 if (*h < minHeight) *h = minHeight; 227 if (*w > maxWidth) *w = maxWidth; 228 if (*h > maxHeight) *h = maxHeight; 229 break; 230 231 case discreteRatioXY: 232 { 233 float scaleX = (*inoutWidth) / (cast(float)defaultWidth); 234 float scaleY = (*inoutHeight) / (cast(float)defaultHeight); 235 scaleX = findBestMatchingFloat(scaleX, discreteScalesX[0..numDiscreteScalesX]); 236 scaleY = findBestMatchingFloat(scaleY, discreteScalesY[0..numDiscreteScalesY]); 237 *inoutWidth = cast(int)(0.5f + scaleX * defaultWidth); 238 *inoutHeight = cast(int)(0.5f + scaleY * defaultHeight); 239 } 240 } 241 } 242 243 /// Given an input size, get a valid size that is the maximum that would fit inside a `inoutWidth` x `inoutHeight`, but smaller. 244 /// Prefer validity if no smaller size is available. 245 void getMaxSmallerValidSize(int* inoutWidth, int* inoutHeight) 246 { 247 final switch(type) with (Type) 248 { 249 case continuousRatio: 250 { 251 // find estimate of scale 252 float scaleX = *inoutWidth / (cast(float)defaultWidth); 253 float scaleY = *inoutHeight / (cast(float)defaultHeight); 254 float scale = (scaleX < scaleY) ? scaleX : scaleY; 255 if (scale < minScale) scale = minScale; 256 if (scale > maxScale) scale = maxScale; 257 *inoutWidth = cast(int)(0.5f + scale * defaultWidth); 258 *inoutHeight = cast(int)(0.5f + scale * defaultHeight); 259 break; 260 } 261 262 case discreteRatio: 263 { 264 // Note: because of ugly rounding issue, we cannot just find the scale from input size. 265 // See Issue #593. Find the best size by generating the size forward and see which one fits. 266 267 int w = 0; 268 int h = 0; 269 270 int bestIndex = 0; // should be the smallest size... not checked 271 float bestScore = float.infinity; 272 for (int n = 0; n < numDiscreteScales; ++n) 273 { 274 // Generate a possible size. 275 int cand_w = cast(int)(0.5f + discreteScales[n] * defaultWidth); 276 int cand_h = cast(int)(0.5f + discreteScales[n] * defaultHeight); 277 278 float scoreX = (*inoutWidth - cand_w); 279 float scoreY = (*inoutHeight - cand_h); 280 float score = scoreX + scoreY; 281 if ( (scoreX >= 0) && (scoreY >= 0) && (score < bestScore) ) 282 { 283 bestScore = score; 284 bestIndex = n; 285 } 286 } 287 288 *inoutWidth = cast(int)(0.5f + discreteScales[bestIndex] * defaultWidth); 289 *inoutHeight = cast(int)(0.5f + discreteScales[bestIndex] * defaultHeight); 290 break; 291 } 292 293 case rectangularBounds: 294 alias w = inoutWidth; 295 alias h = inoutHeight; 296 if (*w < minWidth) *w = minWidth; 297 if (*h < minHeight) *h = minHeight; 298 if (*w > maxWidth) *w = maxWidth; 299 if (*h > maxHeight) *h = maxHeight; 300 break; 301 302 case discreteRatioXY: 303 { 304 // +0.5f since a smaller ratio would lead to a smaller size being generated 305 float scaleX = (*inoutWidth + 0.5f) / (cast(float)defaultWidth); 306 float scaleY = (*inoutHeight + 0.5f) / (cast(float)defaultHeight); 307 scaleX = findMinMatchingFloat(scaleX, discreteScalesX[0..numDiscreteScalesX]); 308 scaleY = findMinMatchingFloat(scaleY, discreteScalesY[0..numDiscreteScalesY]); 309 *inoutWidth = cast(int)(0.5f + scaleX * defaultWidth); 310 *inoutHeight = cast(int)(0.5f + scaleY * defaultHeight); 311 } 312 } 313 } 314 315 private: 316 317 enum MAX_POSSIBLE_SCALES = 12; 318 319 Type type; 320 321 int defaultWidth; 322 int defaultHeight; 323 324 int numDiscreteScales = 0; 325 float[MAX_POSSIBLE_SCALES] discreteScales; // only used with discreteRatio or rectangularBoundsDiscrete case 326 327 alias numDiscreteScalesX = numDiscreteScales; 328 alias discreteScalesX = discreteScales; 329 int numDiscreteScalesY = 0; 330 float[MAX_POSSIBLE_SCALES] discreteScalesY; // only used with rectangularBoundsDiscrete case 331 332 float minScale, maxScale; // only used with continuousRatio case 333 334 int minWidth; // only used in rectangularBounds case 335 int minHeight; 336 int maxWidth; 337 int maxHeight; 338 } 339 340 private: 341 342 // Return arr[n], the nearest element of the array to x 343 static float findBestMatchingFloat(float x, const(float)[] arr) pure @trusted 344 { 345 assert(arr.length > 0); 346 float bestScore = -float.infinity; 347 int bestIndex = 0; 348 for (int n = 0; n < cast(int)arr.length; ++n) 349 { 350 float score = -fast_fabs(arr[n] - x); 351 if (score > bestScore) 352 { 353 bestScore = score; 354 bestIndex = n; 355 } 356 } 357 return arr[bestIndex]; 358 } 359 360 // Return arr[n], the element of the array that approach `threshold` better without exceeding it 361 // (unless every proposed item exceed) 362 static float findMinMatchingFloat(float threshold, const(float)[] arr) pure @trusted 363 { 364 assert(arr.length > 0); 365 float bestScore = float.infinity; 366 int bestIndex = 0; 367 for (int n = 0; n < cast(int)arr.length; ++n) 368 { 369 float score = (threshold - arr[n]); 370 if ( (score >= 0) && (score < bestScore) ) 371 { 372 bestScore = score; 373 bestIndex = n; 374 } 375 } 376 377 // All items were above the threshold, use nearest item. 378 if (bestIndex == -1) 379 return findBestMatchingFloat(threshold, arr); 380 381 return arr[bestIndex]; 382 } 383 384 @trusted unittest 385 { 386 int w, h; 387 388 SizeConstraints a, b; 389 a = makeSizeConstraintsFixed(640, 480); 390 b = a; 391 392 float[3] ratios = [0.5f, 1.0f, 2.0f]; 393 SizeConstraints c = makeSizeConstraintsDiscrete(640, 480, ratios[]); 394 assert(c.isValidSize(640, 480)); 395 396 w = 640*2-1; 397 h = 480-1; 398 c.getMaxSmallerValidSize(&w, &h); 399 assert(w == 320 && h == 240); 400 401 w = 640-1; 402 h = 480; 403 c.getMaxSmallerValidSize(&w, &h); 404 assert(w == 320 && h == 240); 405 406 c = makeSizeConstraintsContinuous(640, 480, 0.5f, 2.0f); 407 assert(c.isValidSize(640, 480)); 408 assert(!c.isValidSize(640/4, 480/4)); 409 assert(c.isValidSize(640/2, 480/2)); 410 assert(c.isValidSize(640*2, 480*2)); 411 412 a.suggestDefaultSize(&w, &h); 413 assert(w == 640 && h == 480); 414 415 float[3] ratiosX = [0.5f, 1.0f, 2.0f]; 416 float[4] ratiosY = [0.5f, 1.0f, 2.0f, 3.0f]; 417 c = makeSizeConstraintsDiscreteXY(900, 500, ratiosX[], ratiosY[]); 418 c.suggestDefaultSize(&w, &h); 419 assert(w == 900 && h == 500); 420 421 w = 100; h = 501; 422 c.getNearestValidSize(&w, &h); 423 assert(w == 450 && h == 500); 424 425 w = 1000; h = 2500; 426 c.getNearestValidSize(&w, &h); 427 assert(w == 900 && h == 1500); 428 } 429 430 unittest 431 { 432 float[4] A = [1.0f, 2, 3, 4]; 433 assert( findMinMatchingFloat(3.8f, A) == 3 ); 434 assert( findMinMatchingFloat(10.0f, A) == 4 ); 435 assert( findMinMatchingFloat(2.0f, A) == 2 ); 436 assert( findMinMatchingFloat(-1.0f, A) == 1 ); 437 assert( findBestMatchingFloat(3.8f, A) == 4 ); 438 assert( findBestMatchingFloat(10.0f, A) == 4 ); 439 assert( findBestMatchingFloat(2.0f, A) == 2 ); 440 assert( findBestMatchingFloat(-1.0f, A) == 1 ); 441 } 442 443 // Issue #593, max min valid size not matching 444 @trusted unittest 445 { 446 static immutable float[6] ratios = [0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f]; 447 SizeConstraints c = makeSizeConstraintsDiscrete(626, 487, ratios); 448 int w = 1096, h = 852; 449 c.getMaxSmallerValidSize(&w, &h); 450 assert(w == 1096 && h == 852); 451 452 // Same but with separate XY 453 c = makeSizeConstraintsDiscreteXY(487, 487, ratios, ratios); 454 w = 852; 455 h = 852; 456 c.getMaxSmallerValidSize(&w, &h); 457 assert(w == 852 && h == 852); 458 }