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 bool canResizeHorizontally() 175 { 176 final switch(type) with (Type) 177 { 178 case continuousRatio: 179 case discreteRatio: 180 return true; 181 case rectangularBounds: 182 return minWidth != maxWidth; 183 case discreteRatioXY: 184 return numDiscreteScalesX > 1; 185 } 186 } 187 188 bool canResizeVertically() 189 { 190 final switch(type) with (Type) 191 { 192 case continuousRatio: 193 case discreteRatio: 194 return true; 195 case rectangularBounds: 196 return minHeight != maxHeight; 197 case discreteRatioXY: 198 return numDiscreteScalesY > 1; 199 } 200 } 201 202 /// Returns `true` if this `SizeConstraints` preserve plugin aspect ratio. 203 bool preserveAspectRatio() 204 { 205 final switch(type) with (Type) 206 { 207 case continuousRatio: 208 case discreteRatio: 209 return true; 210 case rectangularBounds: 211 case discreteRatioXY: 212 return false; 213 } 214 } 215 216 /// _Approximate_ aspect ratio fraction that should be preserved on resize. 217 /// Only make sense if `preserveAspectRatio()` yields true. 218 /// Returns: X and Y, represent X/Y fraction. 219 int[2] aspectRatio() 220 { 221 final switch(type) with (Type) 222 { 223 case continuousRatio: 224 case discreteRatio: 225 return [defaultWidth, defaultHeight]; 226 case rectangularBounds: 227 case discreteRatioXY: 228 // doesn't make sense, there is no single aspect ratio for the UI 229 return [defaultWidth, defaultHeight]; 230 } 231 } 232 233 /// Returns `true` if this `SizeConstraints` allows this size. 234 bool isValidSize(int width, int height) @trusted 235 { 236 int validw = width, 237 validh = height; 238 getNearestValidSize(&validw, &validh); 239 return validw == width && validh == height; // if the input size is valid, will return the same 240 } 241 242 /// Given an input size, get the nearest valid size. 243 void getNearestValidSize(int* inoutWidth, int* inoutHeight) 244 { 245 final switch(type) with (Type) 246 { 247 case continuousRatio: 248 // find estimate of scale 249 float scale = 0.5f * (*inoutWidth / (cast(float)defaultWidth) 250 + *inoutHeight / (cast(float)defaultHeight)); 251 if (scale < minScale) scale = minScale; 252 if (scale > maxScale) scale = maxScale; 253 *inoutWidth = cast(int)(0.5f + scale * defaultWidth); 254 *inoutHeight = cast(int)(0.5f + scale * defaultHeight); 255 break; 256 257 case discreteRatio: 258 { 259 float scale = 0.5f * (*inoutWidth / (cast(float)defaultWidth) 260 + *inoutHeight / (cast(float)defaultHeight)); 261 scale = findBestMatchingFloat(scale, discreteScales[0..numDiscreteScales]); 262 *inoutWidth = cast(int)(0.5f + scale * defaultWidth); 263 *inoutHeight = cast(int)(0.5f + scale * defaultHeight); 264 break; 265 } 266 267 case rectangularBounds: 268 alias w = inoutWidth; 269 alias h = inoutHeight; 270 if (*w < minWidth) *w = minWidth; 271 if (*h < minHeight) *h = minHeight; 272 if (*w > maxWidth) *w = maxWidth; 273 if (*h > maxHeight) *h = maxHeight; 274 break; 275 276 case discreteRatioXY: 277 { 278 float scaleX = (*inoutWidth) / (cast(float)defaultWidth); 279 float scaleY = (*inoutHeight) / (cast(float)defaultHeight); 280 scaleX = findBestMatchingFloat(scaleX, discreteScalesX[0..numDiscreteScalesX]); 281 scaleY = findBestMatchingFloat(scaleY, discreteScalesY[0..numDiscreteScalesY]); 282 *inoutWidth = cast(int)(0.5f + scaleX * defaultWidth); 283 *inoutHeight = cast(int)(0.5f + scaleY * defaultHeight); 284 } 285 } 286 } 287 288 /// Given an input size, get a valid size that is the maximum that would fit inside a `inoutWidth` x `inoutHeight`, but smaller. 289 /// Prefer validity if no smaller size is available. 290 void getMaxSmallerValidSize(int* inoutWidth, int* inoutHeight) 291 { 292 final switch(type) with (Type) 293 { 294 case continuousRatio: 295 { 296 // find estimate of scale 297 float scaleX = *inoutWidth / (cast(float)defaultWidth); 298 float scaleY = *inoutHeight / (cast(float)defaultHeight); 299 float scale = (scaleX < scaleY) ? scaleX : scaleY; 300 if (scale < minScale) scale = minScale; 301 if (scale > maxScale) scale = maxScale; 302 *inoutWidth = cast(int)(0.5f + scale * defaultWidth); 303 *inoutHeight = cast(int)(0.5f + scale * defaultHeight); 304 break; 305 } 306 307 case discreteRatio: 308 { 309 // Note: because of ugly rounding issue, we cannot just find the scale from input size. 310 // See Issue #593. Find the best size by generating the size forward and see which one fits. 311 312 int w = 0; 313 int h = 0; 314 315 int bestIndex = 0; // should be the smallest size... not checked 316 float bestScore = float.infinity; 317 for (int n = 0; n < numDiscreteScales; ++n) 318 { 319 // Generate a possible size. 320 int cand_w = cast(int)(0.5f + discreteScales[n] * defaultWidth); 321 int cand_h = cast(int)(0.5f + discreteScales[n] * defaultHeight); 322 323 float scoreX = (*inoutWidth - cand_w); 324 float scoreY = (*inoutHeight - cand_h); 325 float score = scoreX + scoreY; 326 if ( (scoreX >= 0) && (scoreY >= 0) && (score < bestScore) ) 327 { 328 bestScore = score; 329 bestIndex = n; 330 } 331 } 332 333 *inoutWidth = cast(int)(0.5f + discreteScales[bestIndex] * defaultWidth); 334 *inoutHeight = cast(int)(0.5f + discreteScales[bestIndex] * defaultHeight); 335 break; 336 } 337 338 case rectangularBounds: 339 alias w = inoutWidth; 340 alias h = inoutHeight; 341 if (*w < minWidth) *w = minWidth; 342 if (*h < minHeight) *h = minHeight; 343 if (*w > maxWidth) *w = maxWidth; 344 if (*h > maxHeight) *h = maxHeight; 345 break; 346 347 case discreteRatioXY: 348 { 349 // +0.5f since a smaller ratio would lead to a smaller size being generated 350 float scaleX = (*inoutWidth + 0.5f) / (cast(float)defaultWidth); 351 float scaleY = (*inoutHeight + 0.5f) / (cast(float)defaultHeight); 352 scaleX = findMinMatchingFloat(scaleX, discreteScalesX[0..numDiscreteScalesX]); 353 scaleY = findMinMatchingFloat(scaleY, discreteScalesY[0..numDiscreteScalesY]); 354 *inoutWidth = cast(int)(0.5f + scaleX * defaultWidth); 355 *inoutHeight = cast(int)(0.5f + scaleY * defaultHeight); 356 } 357 } 358 } 359 360 private: 361 362 enum MAX_POSSIBLE_SCALES = 12; 363 364 Type type; 365 366 int defaultWidth; 367 int defaultHeight; 368 369 int numDiscreteScales = 0; 370 float[MAX_POSSIBLE_SCALES] discreteScales; // only used with discreteRatio or rectangularBoundsDiscrete case 371 372 alias numDiscreteScalesX = numDiscreteScales; 373 alias discreteScalesX = discreteScales; 374 int numDiscreteScalesY = 0; 375 float[MAX_POSSIBLE_SCALES] discreteScalesY; // only used with rectangularBoundsDiscrete case 376 377 float minScale, maxScale; // only used with continuousRatio case 378 379 int minWidth; // only used in rectangularBounds case 380 int minHeight; 381 int maxWidth; 382 int maxHeight; 383 } 384 385 private: 386 387 // Return arr[n], the nearest element of the array to x 388 static float findBestMatchingFloat(float x, const(float)[] arr) pure @trusted 389 { 390 assert(arr.length > 0); 391 float bestScore = -float.infinity; 392 int bestIndex = 0; 393 for (int n = 0; n < cast(int)arr.length; ++n) 394 { 395 float score = -fast_fabs(arr[n] - x); 396 if (score > bestScore) 397 { 398 bestScore = score; 399 bestIndex = n; 400 } 401 } 402 return arr[bestIndex]; 403 } 404 405 // Return arr[n], the element of the array that approach `threshold` better without exceeding it 406 // (unless every proposed item exceed) 407 static float findMinMatchingFloat(float threshold, const(float)[] arr) pure @trusted 408 { 409 assert(arr.length > 0); 410 float bestScore = float.infinity; 411 int bestIndex = 0; 412 for (int n = 0; n < cast(int)arr.length; ++n) 413 { 414 float score = (threshold - arr[n]); 415 if ( (score >= 0) && (score < bestScore) ) 416 { 417 bestScore = score; 418 bestIndex = n; 419 } 420 } 421 422 // All items were above the threshold, use nearest item. 423 if (bestIndex == -1) 424 return findBestMatchingFloat(threshold, arr); 425 426 return arr[bestIndex]; 427 } 428 429 @trusted unittest 430 { 431 int w, h; 432 433 SizeConstraints a, b; 434 a = makeSizeConstraintsFixed(640, 480); 435 b = a; 436 437 float[3] ratios = [0.5f, 1.0f, 2.0f]; 438 SizeConstraints c = makeSizeConstraintsDiscrete(640, 480, ratios[]); 439 assert(c.isValidSize(640, 480)); 440 441 w = 640*2-1; 442 h = 480-1; 443 c.getMaxSmallerValidSize(&w, &h); 444 assert(w == 320 && h == 240); 445 446 w = 640-1; 447 h = 480; 448 c.getMaxSmallerValidSize(&w, &h); 449 assert(w == 320 && h == 240); 450 451 c = makeSizeConstraintsContinuous(640, 480, 0.5f, 2.0f); 452 assert(c.isValidSize(640, 480)); 453 assert(!c.isValidSize(640/4, 480/4)); 454 assert(c.isValidSize(640/2, 480/2)); 455 assert(c.isValidSize(640*2, 480*2)); 456 457 a.suggestDefaultSize(&w, &h); 458 assert(w == 640 && h == 480); 459 460 float[3] ratiosX = [0.5f, 1.0f, 2.0f]; 461 float[4] ratiosY = [0.5f, 1.0f, 2.0f, 3.0f]; 462 c = makeSizeConstraintsDiscreteXY(900, 500, ratiosX[], ratiosY[]); 463 c.suggestDefaultSize(&w, &h); 464 assert(w == 900 && h == 500); 465 466 w = 100; h = 501; 467 c.getNearestValidSize(&w, &h); 468 assert(w == 450 && h == 500); 469 470 w = 1000; h = 2500; 471 c.getNearestValidSize(&w, &h); 472 assert(w == 900 && h == 1500); 473 } 474 475 unittest 476 { 477 float[4] A = [1.0f, 2, 3, 4]; 478 assert( findMinMatchingFloat(3.8f, A) == 3 ); 479 assert( findMinMatchingFloat(10.0f, A) == 4 ); 480 assert( findMinMatchingFloat(2.0f, A) == 2 ); 481 assert( findMinMatchingFloat(-1.0f, A) == 1 ); 482 assert( findBestMatchingFloat(3.8f, A) == 4 ); 483 assert( findBestMatchingFloat(10.0f, A) == 4 ); 484 assert( findBestMatchingFloat(2.0f, A) == 2 ); 485 assert( findBestMatchingFloat(-1.0f, A) == 1 ); 486 } 487 488 // Issue #593, max min valid size not matching 489 @trusted unittest 490 { 491 static immutable float[6] ratios = [0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f]; 492 SizeConstraints c = makeSizeConstraintsDiscrete(626, 487, ratios); 493 int w = 1096, h = 852; 494 c.getMaxSmallerValidSize(&w, &h); 495 assert(w == 1096 && h == 852); 496 497 // Same but with separate XY 498 c = makeSizeConstraintsDiscreteXY(487, 487, ratios, ratios); 499 w = 852; 500 h = 852; 501 c.getMaxSmallerValidSize(&w, &h); 502 assert(w == 852 && h == 852); 503 }