UNPKG

16.2 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3/*-----------------------------------------------------------------------------
4| Copyright (c) 2014-2017, PhosphorJS Contributors
5|
6| Distributed under the terms of the BSD 3-Clause License.
7|
8| The full license is in the file LICENSE, distributed with this software.
9|----------------------------------------------------------------------------*/
10
11/**
12 * A sizer object for use with the box engine layout functions.
13 *
14 * #### Notes
15 * A box sizer holds the geometry information for an object along an
16 * arbitrary layout orientation.
17 *
18 * For best performance, this class should be treated as a raw data
19 * struct. It should not typically be subclassed.
20 */
21export class BoxSizer {
22 /**
23 * The preferred size for the sizer.
24 *
25 * #### Notes
26 * The sizer will be given this initial size subject to its size
27 * bounds. The sizer will not deviate from this size unless such
28 * deviation is required to fit into the available layout space.
29 *
30 * There is no limit to this value, but it will be clamped to the
31 * bounds defined by [[minSize]] and [[maxSize]].
32 *
33 * The default value is `0`.
34 */
35 sizeHint = 0;
36
37 /**
38 * The minimum size of the sizer.
39 *
40 * #### Notes
41 * The sizer will never be sized less than this value, even if
42 * it means the sizer will overflow the available layout space.
43 *
44 * It is assumed that this value lies in the range `[0, Infinity)`
45 * and that it is `<=` to [[maxSize]]. Failure to adhere to this
46 * constraint will yield undefined results.
47 *
48 * The default value is `0`.
49 */
50 minSize = 0;
51
52 /**
53 * The maximum size of the sizer.
54 *
55 * #### Notes
56 * The sizer will never be sized greater than this value, even if
57 * it means the sizer will underflow the available layout space.
58 *
59 * It is assumed that this value lies in the range `[0, Infinity]`
60 * and that it is `>=` to [[minSize]]. Failure to adhere to this
61 * constraint will yield undefined results.
62 *
63 * The default value is `Infinity`.
64 */
65 maxSize = Infinity;
66
67 /**
68 * The stretch factor for the sizer.
69 *
70 * #### Notes
71 * This controls how much the sizer stretches relative to its sibling
72 * sizers when layout space is distributed. A stretch factor of zero
73 * is special and will cause the sizer to only be resized after all
74 * other sizers with a stretch factor greater than zero have been
75 * resized to their limits.
76 *
77 * It is assumed that this value is an integer that lies in the range
78 * `[0, Infinity)`. Failure to adhere to this constraint will yield
79 * undefined results.
80 *
81 * The default value is `1`.
82 */
83 stretch = 1;
84
85 /**
86 * The computed size of the sizer.
87 *
88 * #### Notes
89 * This value is the output of a call to [[boxCalc]]. It represents
90 * the computed size for the object along the layout orientation,
91 * and will always lie in the range `[minSize, maxSize]`.
92 *
93 * This value is output only.
94 *
95 * Changing this value will have no effect.
96 */
97 size = 0;
98
99 /**
100 * An internal storage property for the layout algorithm.
101 *
102 * #### Notes
103 * This value is used as temporary storage by the layout algorithm.
104 *
105 * Changing this value will have no effect.
106 */
107 done = false;
108}
109
110/**
111 * The namespace for the box engine layout functions.
112 */
113export namespace BoxEngine {
114 /**
115 * Calculate the optimal layout sizes for a sequence of box sizers.
116 *
117 * This distributes the available layout space among the box sizers
118 * according to the following algorithm:
119 *
120 * 1. Initialize the sizers's size to its size hint and compute the
121 * sums for each of size hint, min size, and max size.
122 *
123 * 2. If the total size hint equals the available space, return.
124 *
125 * 3. If the available space is less than the total min size, set all
126 * sizers to their min size and return.
127 *
128 * 4. If the available space is greater than the total max size, set
129 * all sizers to their max size and return.
130 *
131 * 5. If the layout space is less than the total size hint, distribute
132 * the negative delta as follows:
133 *
134 * a. Shrink each sizer with a stretch factor greater than zero by
135 * an amount proportional to the negative space and the sum of
136 * stretch factors. If the sizer reaches its min size, remove
137 * it and its stretch factor from the computation.
138 *
139 * b. If after adjusting all stretch sizers there remains negative
140 * space, distribute the space equally among the sizers with a
141 * stretch factor of zero. If a sizer reaches its min size,
142 * remove it from the computation.
143 *
144 * 6. If the layout space is greater than the total size hint,
145 * distribute the positive delta as follows:
146 *
147 * a. Expand each sizer with a stretch factor greater than zero by
148 * an amount proportional to the postive space and the sum of
149 * stretch factors. If the sizer reaches its max size, remove
150 * it and its stretch factor from the computation.
151 *
152 * b. If after adjusting all stretch sizers there remains positive
153 * space, distribute the space equally among the sizers with a
154 * stretch factor of zero. If a sizer reaches its max size,
155 * remove it from the computation.
156 *
157 * 7. return
158 *
159 * @param sizers - The sizers for a particular layout line.
160 *
161 * @param space - The available layout space for the sizers.
162 *
163 * @returns The delta between the provided available space and the
164 * actual consumed space. This value will be zero if the sizers
165 * can be adjusted to fit, negative if the available space is too
166 * small, and positive if the available space is too large.
167 *
168 * #### Notes
169 * The [[size]] of each sizer is updated with the computed size.
170 *
171 * This function can be called at any time to recompute the layout for
172 * an existing sequence of sizers. The previously computed results will
173 * have no effect on the new output. It is therefore not necessary to
174 * create new sizer objects on each resize event.
175 */
176 export function calc(sizers: ArrayLike<BoxSizer>, space: number): number {
177 // Bail early if there is nothing to do.
178 let count = sizers.length;
179 if (count === 0) {
180 return space;
181 }
182
183 // Setup the size and stretch counters.
184 let totalMin = 0;
185 let totalMax = 0;
186 let totalSize = 0;
187 let totalStretch = 0;
188 let stretchCount = 0;
189
190 // Setup the sizers and compute the totals.
191 for (let i = 0; i < count; ++i) {
192 let sizer = sizers[i];
193 let min = sizer.minSize;
194 let max = sizer.maxSize;
195 let hint = sizer.sizeHint;
196 sizer.done = false;
197 sizer.size = Math.max(min, Math.min(hint, max));
198 totalSize += sizer.size;
199 totalMin += min;
200 totalMax += max;
201 if (sizer.stretch > 0) {
202 totalStretch += sizer.stretch;
203 stretchCount++;
204 }
205 }
206
207 // If the space is equal to the total size, return early.
208 if (space === totalSize) {
209 return 0;
210 }
211
212 // If the space is less than the total min, minimize each sizer.
213 if (space <= totalMin) {
214 for (let i = 0; i < count; ++i) {
215 let sizer = sizers[i];
216 sizer.size = sizer.minSize;
217 }
218 return space - totalMin;
219 }
220
221 // If the space is greater than the total max, maximize each sizer.
222 if (space >= totalMax) {
223 for (let i = 0; i < count; ++i) {
224 let sizer = sizers[i];
225 sizer.size = sizer.maxSize;
226 }
227 return space - totalMax;
228 }
229
230 // The loops below perform sub-pixel precision sizing. A near zero
231 // value is used for compares instead of zero to ensure that the
232 // loop terminates when the subdivided space is reasonably small.
233 let nearZero = 0.01;
234
235 // A counter which is decremented each time a sizer is resized to
236 // its limit. This ensures the loops terminate even if there is
237 // space remaining to distribute.
238 let notDoneCount = count;
239
240 // Distribute negative delta space.
241 if (space < totalSize) {
242 // Shrink each stretchable sizer by an amount proportional to its
243 // stretch factor. If a sizer reaches its min size it's marked as
244 // done. The loop progresses in phases where each sizer is given
245 // a chance to consume its fair share for the pass, regardless of
246 // whether a sizer before it reached its limit. This continues
247 // until the stretchable sizers or the free space is exhausted.
248 let freeSpace = totalSize - space;
249 while (stretchCount > 0 && freeSpace > nearZero) {
250 let distSpace = freeSpace;
251 let distStretch = totalStretch;
252 for (let i = 0; i < count; ++i) {
253 let sizer = sizers[i];
254 if (sizer.done || sizer.stretch === 0) {
255 continue;
256 }
257 let amt = (sizer.stretch * distSpace) / distStretch;
258 if (sizer.size - amt <= sizer.minSize) {
259 freeSpace -= sizer.size - sizer.minSize;
260 totalStretch -= sizer.stretch;
261 sizer.size = sizer.minSize;
262 sizer.done = true;
263 notDoneCount--;
264 stretchCount--;
265 } else {
266 freeSpace -= amt;
267 sizer.size -= amt;
268 }
269 }
270 }
271 // Distribute any remaining space evenly among the non-stretchable
272 // sizers. This progresses in phases in the same manner as above.
273 while (notDoneCount > 0 && freeSpace > nearZero) {
274 let amt = freeSpace / notDoneCount;
275 for (let i = 0; i < count; ++i) {
276 let sizer = sizers[i];
277 if (sizer.done) {
278 continue;
279 }
280 if (sizer.size - amt <= sizer.minSize) {
281 freeSpace -= sizer.size - sizer.minSize;
282 sizer.size = sizer.minSize;
283 sizer.done = true;
284 notDoneCount--;
285 } else {
286 freeSpace -= amt;
287 sizer.size -= amt;
288 }
289 }
290 }
291 }
292 // Distribute positive delta space.
293 else {
294 // Expand each stretchable sizer by an amount proportional to its
295 // stretch factor. If a sizer reaches its max size it's marked as
296 // done. The loop progresses in phases where each sizer is given
297 // a chance to consume its fair share for the pass, regardless of
298 // whether a sizer before it reached its limit. This continues
299 // until the stretchable sizers or the free space is exhausted.
300 let freeSpace = space - totalSize;
301 while (stretchCount > 0 && freeSpace > nearZero) {
302 let distSpace = freeSpace;
303 let distStretch = totalStretch;
304 for (let i = 0; i < count; ++i) {
305 let sizer = sizers[i];
306 if (sizer.done || sizer.stretch === 0) {
307 continue;
308 }
309 let amt = (sizer.stretch * distSpace) / distStretch;
310 if (sizer.size + amt >= sizer.maxSize) {
311 freeSpace -= sizer.maxSize - sizer.size;
312 totalStretch -= sizer.stretch;
313 sizer.size = sizer.maxSize;
314 sizer.done = true;
315 notDoneCount--;
316 stretchCount--;
317 } else {
318 freeSpace -= amt;
319 sizer.size += amt;
320 }
321 }
322 }
323 // Distribute any remaining space evenly among the non-stretchable
324 // sizers. This progresses in phases in the same manner as above.
325 while (notDoneCount > 0 && freeSpace > nearZero) {
326 let amt = freeSpace / notDoneCount;
327 for (let i = 0; i < count; ++i) {
328 let sizer = sizers[i];
329 if (sizer.done) {
330 continue;
331 }
332 if (sizer.size + amt >= sizer.maxSize) {
333 freeSpace -= sizer.maxSize - sizer.size;
334 sizer.size = sizer.maxSize;
335 sizer.done = true;
336 notDoneCount--;
337 } else {
338 freeSpace -= amt;
339 sizer.size += amt;
340 }
341 }
342 }
343 }
344
345 // Indicate that the consumed space equals the available space.
346 return 0;
347 }
348
349 /**
350 * Adjust a sizer by a delta and update its neighbors accordingly.
351 *
352 * @param sizers - The sizers which should be adjusted.
353 *
354 * @param index - The index of the sizer to grow.
355 *
356 * @param delta - The amount to adjust the sizer, positive or negative.
357 *
358 * #### Notes
359 * This will adjust the indicated sizer by the specified amount, along
360 * with the sizes of the appropriate neighbors, subject to the limits
361 * specified by each of the sizers.
362 *
363 * This is useful when implementing box layouts where the boundaries
364 * between the sizers are interactively adjustable by the user.
365 */
366 export function adjust(
367 sizers: ArrayLike<BoxSizer>,
368 index: number,
369 delta: number
370 ): void {
371 // Bail early when there is nothing to do.
372 if (sizers.length === 0 || delta === 0) {
373 return;
374 }
375
376 // Dispatch to the proper implementation.
377 if (delta > 0) {
378 growSizer(sizers, index, delta);
379 } else {
380 shrinkSizer(sizers, index, -delta);
381 }
382 }
383
384 /**
385 * Grow a sizer by a positive delta and adjust neighbors.
386 */
387 function growSizer(
388 sizers: ArrayLike<BoxSizer>,
389 index: number,
390 delta: number
391 ): void {
392 // Compute how much the items to the left can expand.
393 let growLimit = 0;
394 for (let i = 0; i <= index; ++i) {
395 let sizer = sizers[i];
396 growLimit += sizer.maxSize - sizer.size;
397 }
398
399 // Compute how much the items to the right can shrink.
400 let shrinkLimit = 0;
401 for (let i = index + 1, n = sizers.length; i < n; ++i) {
402 let sizer = sizers[i];
403 shrinkLimit += sizer.size - sizer.minSize;
404 }
405
406 // Clamp the delta adjustment to the limits.
407 delta = Math.min(delta, growLimit, shrinkLimit);
408
409 // Grow the sizers to the left by the delta.
410 let grow = delta;
411 for (let i = index; i >= 0 && grow > 0; --i) {
412 let sizer = sizers[i];
413 let limit = sizer.maxSize - sizer.size;
414 if (limit >= grow) {
415 sizer.sizeHint = sizer.size + grow;
416 grow = 0;
417 } else {
418 sizer.sizeHint = sizer.size + limit;
419 grow -= limit;
420 }
421 }
422
423 // Shrink the sizers to the right by the delta.
424 let shrink = delta;
425 for (let i = index + 1, n = sizers.length; i < n && shrink > 0; ++i) {
426 let sizer = sizers[i];
427 let limit = sizer.size - sizer.minSize;
428 if (limit >= shrink) {
429 sizer.sizeHint = sizer.size - shrink;
430 shrink = 0;
431 } else {
432 sizer.sizeHint = sizer.size - limit;
433 shrink -= limit;
434 }
435 }
436 }
437
438 /**
439 * Shrink a sizer by a positive delta and adjust neighbors.
440 */
441 function shrinkSizer(
442 sizers: ArrayLike<BoxSizer>,
443 index: number,
444 delta: number
445 ): void {
446 // Compute how much the items to the right can expand.
447 let growLimit = 0;
448 for (let i = index + 1, n = sizers.length; i < n; ++i) {
449 let sizer = sizers[i];
450 growLimit += sizer.maxSize - sizer.size;
451 }
452
453 // Compute how much the items to the left can shrink.
454 let shrinkLimit = 0;
455 for (let i = 0; i <= index; ++i) {
456 let sizer = sizers[i];
457 shrinkLimit += sizer.size - sizer.minSize;
458 }
459
460 // Clamp the delta adjustment to the limits.
461 delta = Math.min(delta, growLimit, shrinkLimit);
462
463 // Grow the sizers to the right by the delta.
464 let grow = delta;
465 for (let i = index + 1, n = sizers.length; i < n && grow > 0; ++i) {
466 let sizer = sizers[i];
467 let limit = sizer.maxSize - sizer.size;
468 if (limit >= grow) {
469 sizer.sizeHint = sizer.size + grow;
470 grow = 0;
471 } else {
472 sizer.sizeHint = sizer.size + limit;
473 grow -= limit;
474 }
475 }
476
477 // Shrink the sizers to the left by the delta.
478 let shrink = delta;
479 for (let i = index; i >= 0 && shrink > 0; --i) {
480 let sizer = sizers[i];
481 let limit = sizer.size - sizer.minSize;
482 if (limit >= shrink) {
483 sizer.sizeHint = sizer.size - shrink;
484 shrink = 0;
485 } else {
486 sizer.sizeHint = sizer.size - limit;
487 shrink -= limit;
488 }
489 }
490 }
491}