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 | */
|
21 | export 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 | */
|
113 | export 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 | }
|