UNPKG

@lumino/widgets

Version:
492 lines (463 loc) 16.2 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2017, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ /** * A sizer object for use with the box engine layout functions. * * #### Notes * A box sizer holds the geometry information for an object along an * arbitrary layout orientation. * * For best performance, this class should be treated as a raw data * struct. It should not typically be subclassed. */ export class BoxSizer { /** * The preferred size for the sizer. * * #### Notes * The sizer will be given this initial size subject to its size * bounds. The sizer will not deviate from this size unless such * deviation is required to fit into the available layout space. * * There is no limit to this value, but it will be clamped to the * bounds defined by {@link minSize} and {@link maxSize}. * * The default value is `0`. */ sizeHint = 0; /** * The minimum size of the sizer. * * #### Notes * The sizer will never be sized less than this value, even if * it means the sizer will overflow the available layout space. * * It is assumed that this value lies in the range `[0, Infinity)` * and that it is `<=` to {@link maxSize}. Failure to adhere to this * constraint will yield undefined results. * * The default value is `0`. */ minSize = 0; /** * The maximum size of the sizer. * * #### Notes * The sizer will never be sized greater than this value, even if * it means the sizer will underflow the available layout space. * * It is assumed that this value lies in the range `[0, Infinity]` * and that it is `>=` to {@link minSize}. Failure to adhere to this * constraint will yield undefined results. * * The default value is `Infinity`. */ maxSize = Infinity; /** * The stretch factor for the sizer. * * #### Notes * This controls how much the sizer stretches relative to its sibling * sizers when layout space is distributed. A stretch factor of zero * is special and will cause the sizer to only be resized after all * other sizers with a stretch factor greater than zero have been * resized to their limits. * * It is assumed that this value is an integer that lies in the range * `[0, Infinity)`. Failure to adhere to this constraint will yield * undefined results. * * The default value is `1`. */ stretch = 1; /** * The computed size of the sizer. * * #### Notes * This value is the output of a call to {@link BoxEngine.calc}. It represents * the computed size for the object along the layout orientation, * and will always lie in the range `[minSize, maxSize]`. * * This value is output only. * * Changing this value will have no effect. */ size = 0; /** * An internal storage property for the layout algorithm. * * #### Notes * This value is used as temporary storage by the layout algorithm. * * Changing this value will have no effect. */ done = false; } /** * The namespace for the box engine layout functions. */ export namespace BoxEngine { /** * Calculate the optimal layout sizes for a sequence of box sizers. * * This distributes the available layout space among the box sizers * according to the following algorithm: * * 1. Initialize the sizers's size to its size hint and compute the * sums for each of size hint, min size, and max size. * * 2. If the total size hint equals the available space, return. * * 3. If the available space is less than the total min size, set all * sizers to their min size and return. * * 4. If the available space is greater than the total max size, set * all sizers to their max size and return. * * 5. If the layout space is less than the total size hint, distribute * the negative delta as follows: * * a. Shrink each sizer with a stretch factor greater than zero by * an amount proportional to the negative space and the sum of * stretch factors. If the sizer reaches its min size, remove * it and its stretch factor from the computation. * * b. If after adjusting all stretch sizers there remains negative * space, distribute the space equally among the sizers with a * stretch factor of zero. If a sizer reaches its min size, * remove it from the computation. * * 6. If the layout space is greater than the total size hint, * distribute the positive delta as follows: * * a. Expand each sizer with a stretch factor greater than zero by * an amount proportional to the postive space and the sum of * stretch factors. If the sizer reaches its max size, remove * it and its stretch factor from the computation. * * b. If after adjusting all stretch sizers there remains positive * space, distribute the space equally among the sizers with a * stretch factor of zero. If a sizer reaches its max size, * remove it from the computation. * * 7. return * * @param sizers - The sizers for a particular layout line. * * @param space - The available layout space for the sizers. * * @returns The delta between the provided available space and the * actual consumed space. This value will be zero if the sizers * can be adjusted to fit, negative if the available space is too * small, and positive if the available space is too large. * * #### Notes * The {@link BoxSizer.size} of each sizer is updated with the computed size. * * This function can be called at any time to recompute the layout for * an existing sequence of sizers. The previously computed results will * have no effect on the new output. It is therefore not necessary to * create new sizer objects on each resize event. */ export function calc(sizers: ArrayLike<BoxSizer>, space: number): number { // Bail early if there is nothing to do. let count = sizers.length; if (count === 0) { return space; } // Setup the size and stretch counters. let totalMin = 0; let totalMax = 0; let totalSize = 0; let totalStretch = 0; let stretchCount = 0; // Setup the sizers and compute the totals. for (let i = 0; i < count; ++i) { let sizer = sizers[i]; let min = sizer.minSize; let max = sizer.maxSize; let hint = sizer.sizeHint; sizer.done = false; sizer.size = Math.max(min, Math.min(hint, max)); totalSize += sizer.size; totalMin += min; totalMax += max; if (sizer.stretch > 0) { totalStretch += sizer.stretch; stretchCount++; } } // If the space is equal to the total size, return early. if (space === totalSize) { return 0; } // If the space is less than the total min, minimize each sizer. if (space <= totalMin) { for (let i = 0; i < count; ++i) { let sizer = sizers[i]; sizer.size = sizer.minSize; } return space - totalMin; } // If the space is greater than the total max, maximize each sizer. if (space >= totalMax) { for (let i = 0; i < count; ++i) { let sizer = sizers[i]; sizer.size = sizer.maxSize; } return space - totalMax; } // The loops below perform sub-pixel precision sizing. A near zero // value is used for compares instead of zero to ensure that the // loop terminates when the subdivided space is reasonably small. let nearZero = 0.01; // A counter which is decremented each time a sizer is resized to // its limit. This ensures the loops terminate even if there is // space remaining to distribute. let notDoneCount = count; // Distribute negative delta space. if (space < totalSize) { // Shrink each stretchable sizer by an amount proportional to its // stretch factor. If a sizer reaches its min size it's marked as // done. The loop progresses in phases where each sizer is given // a chance to consume its fair share for the pass, regardless of // whether a sizer before it reached its limit. This continues // until the stretchable sizers or the free space is exhausted. let freeSpace = totalSize - space; while (stretchCount > 0 && freeSpace > nearZero) { let distSpace = freeSpace; let distStretch = totalStretch; for (let i = 0; i < count; ++i) { let sizer = sizers[i]; if (sizer.done || sizer.stretch === 0) { continue; } let amt = (sizer.stretch * distSpace) / distStretch; if (sizer.size - amt <= sizer.minSize) { freeSpace -= sizer.size - sizer.minSize; totalStretch -= sizer.stretch; sizer.size = sizer.minSize; sizer.done = true; notDoneCount--; stretchCount--; } else { freeSpace -= amt; sizer.size -= amt; } } } // Distribute any remaining space evenly among the non-stretchable // sizers. This progresses in phases in the same manner as above. while (notDoneCount > 0 && freeSpace > nearZero) { let amt = freeSpace / notDoneCount; for (let i = 0; i < count; ++i) { let sizer = sizers[i]; if (sizer.done) { continue; } if (sizer.size - amt <= sizer.minSize) { freeSpace -= sizer.size - sizer.minSize; sizer.size = sizer.minSize; sizer.done = true; notDoneCount--; } else { freeSpace -= amt; sizer.size -= amt; } } } } // Distribute positive delta space. else { // Expand each stretchable sizer by an amount proportional to its // stretch factor. If a sizer reaches its max size it's marked as // done. The loop progresses in phases where each sizer is given // a chance to consume its fair share for the pass, regardless of // whether a sizer before it reached its limit. This continues // until the stretchable sizers or the free space is exhausted. let freeSpace = space - totalSize; while (stretchCount > 0 && freeSpace > nearZero) { let distSpace = freeSpace; let distStretch = totalStretch; for (let i = 0; i < count; ++i) { let sizer = sizers[i]; if (sizer.done || sizer.stretch === 0) { continue; } let amt = (sizer.stretch * distSpace) / distStretch; if (sizer.size + amt >= sizer.maxSize) { freeSpace -= sizer.maxSize - sizer.size; totalStretch -= sizer.stretch; sizer.size = sizer.maxSize; sizer.done = true; notDoneCount--; stretchCount--; } else { freeSpace -= amt; sizer.size += amt; } } } // Distribute any remaining space evenly among the non-stretchable // sizers. This progresses in phases in the same manner as above. while (notDoneCount > 0 && freeSpace > nearZero) { let amt = freeSpace / notDoneCount; for (let i = 0; i < count; ++i) { let sizer = sizers[i]; if (sizer.done) { continue; } if (sizer.size + amt >= sizer.maxSize) { freeSpace -= sizer.maxSize - sizer.size; sizer.size = sizer.maxSize; sizer.done = true; notDoneCount--; } else { freeSpace -= amt; sizer.size += amt; } } } } // Indicate that the consumed space equals the available space. return 0; } /** * Adjust a sizer by a delta and update its neighbors accordingly. * * @param sizers - The sizers which should be adjusted. * * @param index - The index of the sizer to grow. * * @param delta - The amount to adjust the sizer, positive or negative. * * #### Notes * This will adjust the indicated sizer by the specified amount, along * with the sizes of the appropriate neighbors, subject to the limits * specified by each of the sizers. * * This is useful when implementing box layouts where the boundaries * between the sizers are interactively adjustable by the user. */ export function adjust( sizers: ArrayLike<BoxSizer>, index: number, delta: number ): void { // Bail early when there is nothing to do. if (sizers.length === 0 || delta === 0) { return; } // Dispatch to the proper implementation. if (delta > 0) { growSizer(sizers, index, delta); } else { shrinkSizer(sizers, index, -delta); } } /** * Grow a sizer by a positive delta and adjust neighbors. */ function growSizer( sizers: ArrayLike<BoxSizer>, index: number, delta: number ): void { // Compute how much the items to the left can expand. let growLimit = 0; for (let i = 0; i <= index; ++i) { let sizer = sizers[i]; growLimit += sizer.maxSize - sizer.size; } // Compute how much the items to the right can shrink. let shrinkLimit = 0; for (let i = index + 1, n = sizers.length; i < n; ++i) { let sizer = sizers[i]; shrinkLimit += sizer.size - sizer.minSize; } // Clamp the delta adjustment to the limits. delta = Math.min(delta, growLimit, shrinkLimit); // Grow the sizers to the left by the delta. let grow = delta; for (let i = index; i >= 0 && grow > 0; --i) { let sizer = sizers[i]; let limit = sizer.maxSize - sizer.size; if (limit >= grow) { sizer.sizeHint = sizer.size + grow; grow = 0; } else { sizer.sizeHint = sizer.size + limit; grow -= limit; } } // Shrink the sizers to the right by the delta. let shrink = delta; for (let i = index + 1, n = sizers.length; i < n && shrink > 0; ++i) { let sizer = sizers[i]; let limit = sizer.size - sizer.minSize; if (limit >= shrink) { sizer.sizeHint = sizer.size - shrink; shrink = 0; } else { sizer.sizeHint = sizer.size - limit; shrink -= limit; } } } /** * Shrink a sizer by a positive delta and adjust neighbors. */ function shrinkSizer( sizers: ArrayLike<BoxSizer>, index: number, delta: number ): void { // Compute how much the items to the right can expand. let growLimit = 0; for (let i = index + 1, n = sizers.length; i < n; ++i) { let sizer = sizers[i]; growLimit += sizer.maxSize - sizer.size; } // Compute how much the items to the left can shrink. let shrinkLimit = 0; for (let i = 0; i <= index; ++i) { let sizer = sizers[i]; shrinkLimit += sizer.size - sizer.minSize; } // Clamp the delta adjustment to the limits. delta = Math.min(delta, growLimit, shrinkLimit); // Grow the sizers to the right by the delta. let grow = delta; for (let i = index + 1, n = sizers.length; i < n && grow > 0; ++i) { let sizer = sizers[i]; let limit = sizer.maxSize - sizer.size; if (limit >= grow) { sizer.sizeHint = sizer.size + grow; grow = 0; } else { sizer.sizeHint = sizer.size + limit; grow -= limit; } } // Shrink the sizers to the left by the delta. let shrink = delta; for (let i = index; i >= 0 && shrink > 0; --i) { let sizer = sizers[i]; let limit = sizer.size - sizer.minSize; if (limit >= shrink) { sizer.sizeHint = sizer.size - shrink; shrink = 0; } else { sizer.sizeHint = sizer.size - limit; shrink -= limit; } } } }