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 }