Home Reference Source

src/utils/view/LayoutHelpers.js

/**
 * Created by lundfall on 01/09/16.
 */
import isEqual                      from 'lodash/isEqual.js';
import findIndex                    from 'lodash/findIndex.js';
import Easing                       from 'famous/transitions/Easing.js';

import {Utils}                      from './Utils.js';
import {TrueSizedLayoutDockHelper}  from '../../layout/TrueSizedLayoutDockHelper.js';


class BaseLayoutHelper {
    constructor(sizeResolver) {
        this._sizeResolver = sizeResolver;
    }

    layout() {
        throw Error("Not implemented");
    }

    boundingBoxSize() {
        throw Error("Not implemented");
    }

    /**
     * Gets the flow information from the renderable
     * @param {Renderable} renderable
     * @returns {{transition: Object, callback: Function}}
     * @private
     */
    _getRenderableFlowInformation(renderable) {
        let {decorations} = renderable;
        let flowInformation = {transition: undefined, callback: undefined};
        let {flow} = decorations;
        if (flow) {
            flowInformation.transition = flow.currentTransition || (flow.defaults && flow.defaults.transition);
            flowInformation.callback = flow.callback;
        }
        return flowInformation;
    }
}


export class DockedLayoutHelper extends BaseLayoutHelper {

    /**
     * Computes translation, inner size, actual docking size (outer size) and an adjusted docking size for a renderable that is about to be docked.
     * @param {OrderedHashMap} dockedRenderables A map containing Array-pairs of [renderable, renderableCounterpart] containing the things that are attached to the sides.
     * @param {OrderedHashMap} filledRenderables A map containing Array-pairs of [renderable, renderableCounterpart] containing the things that are filled.
     * @param {Object} context. The famous context with a valid size proportion.
     * @param {Object} ownDecorators The decorators that are applied to the view.
     * @param {Array|Number} [ownDecorators.extraTranslate]. A translate to shift the entire layout with.
     * @param {Array|Number} [ownDecorators.viewMargins] The margins to apply on the outer edges of the view.
     * @returns {undefined}
     * @private
     */
    layout(dockedRenderables, filledRenderables, context, ownDecorations) {
        let {extraTranslate, viewMargins: margins} = ownDecorations;
        let dockHelper = new TrueSizedLayoutDockHelper(context);

        if (margins) {
            dockHelper.margins(margins);
        }

        /* Process Renderables with a non-fill dock */
        let dockedNames = dockedRenderables ? dockedRenderables.keys() : [];
        for (let renderableName of dockedNames) {
            let [renderable, renderableCounterpart] = dockedRenderables.get(renderableName);
            let {dockSize, translate, innerSize, space} = this._prepareForDockedRenderable(renderable, renderableCounterpart, context, extraTranslate, margins);
            let {callback, transition} = this._getRenderableFlowInformation(renderable);
            let {dock, rotate, opacity, origin, scale, skew} = renderable.decorations; // todo add scaling/skew
            let {dockMethod} = dock;
            if (dockHelper[dockMethod]) {
                dockHelper[dockMethod](renderableName, dockSize, space, translate, innerSize, {
                    rotate,
                    opacity,
                    callback,
                    transition,
                    origin,
                    scale,
                    skew
                });
            }
        }

        /* Process Renderables with a fill dock (this needs to be done after non-fill docks, since order matters in LayoutDockHelper) */
        let filledNames = filledRenderables ? filledRenderables.keys() : [];
        for (let renderableName of filledNames) {
            let [renderable, renderableCounterpart] = filledRenderables.get(renderableName);
            let {decorations} = renderable;
            let {rotate, opacity, origin} = decorations;
            let {translate, dockSize} = this._prepareForDockedRenderable(renderable, renderableCounterpart, context, extraTranslate, margins);
            let {callback, transition} = this._getRenderableFlowInformation(renderable);
            /* Special case for undefined size, since it's treated differently by the dockhelper, and should be kept to undefined if specified */
            let dimensionHasUndefinedSize = (dimension) => [decorations.dock.size, decorations.size].every((size) => !size || size[dimension] === undefined);
            dockSize = dockSize.map((fallbackSize, dimension) => dimensionHasUndefinedSize(dimension) ? undefined : fallbackSize);
            dockHelper.fill(renderableName, dockSize, translate, {rotate, opacity, origin, callback, transition});
        }
    }

    /**
     * Computes translation, inner size, actual docking size (outer size) and an adjusted docking size for a renderable that is about to be docked
     * @param {Renderable} renderable The renderable that is going to be docked
     * @param {Renderable} renderableCounterpart. The renderable counter-part (e.g. AnimationController, RenderNode, or ContainerSurface).
     * @param {Object} context. The famous context with a valid size proportion
     * @param {Array|Number} extraTranslate. A translate to shift the entire layout with
     * @param {Array|Nuimber} margins The margins to apply on the outer edges of the view
     * @returns {{dockSize: (Array|Object), translate, innerSize: (Array|Number), inUseDockSize: (Array|Number}}
     * @private
     */
    _prepareForDockedRenderable(renderable, renderableCounterpart, context, extraTranslate, margins = [0, 0, 0, 0]) {
        let {decorations} = renderable;
        let {translate = [0, 0, 0]} = decorations;
        translate = Utils.addTranslations(extraTranslate, translate);
        let {dockMethod, space} = decorations.dock;
        let horizontalMargins = margins[1] + margins[3];
        let verticalMargins = margins[0] + margins[2];
        let sizeWithoutMargins = [context.size[0] - horizontalMargins, context.size[1] - verticalMargins];
        let dockSizeSpecified = !(isEqual(decorations.dock.size, [undefined, undefined]));
        let dockSize = this._sizeResolver.settleDecoratedSize(renderable, renderableCounterpart, {size: sizeWithoutMargins}, dockSizeSpecified ? decorations.dock.size : decorations.size);
        let inUseDockSize = this._sizeResolver.getResolvedSize(renderable);
        let innerSize;
        let {origin, align} = decorations;
        /* If origin and align is used, we have to add this to the translate of the renderable */
        if (decorations.size || origin || align) {

            let translateWithProportion = (proportion, size, translation, dimension, factor) =>
                translation[dimension] += size[dimension] ? factor * size[dimension] * proportion[dimension] : 0;


            if(decorations.size){

                this._sizeResolver.settleDecoratedSize(renderable, renderableCounterpart, {size: sizeWithoutMargins}, decorations.size);
                innerSize = this._sizeResolver.getResolvedSize(renderable);


                translate = [...translate]; //shallow copy the translation to prevent the translation for happening multiple times

                /* If no docksize was specified in a certain direction, then use the context size without margins */
                let outerDockSize = dockSize;

                if (!dockSizeSpecified) {
                    if (dockMethod === 'fill') {
                        outerDockSize = [...sizeWithoutMargins];
                    } else {
                        let dockingDirection = this.getDockType(dockMethod);
                        outerDockSize[dockingDirection] = innerSize[dockingDirection];
                        outerDockSize[+!dockingDirection] = sizeWithoutMargins[+!dockingDirection];
                    }
                }

                if (origin && decorations.size) {
                    decorations.size.forEach((size, dimension) => {
                        if (this._sizeResolver.isValueTrueSized(size)) {
                            /* Because the size is set to true, it is interpreted as 1 by famous. We have to add 1 pixel
                             *  to make up for this.
                             */
                            translate[dimension] += 1;
                        }
                    });
                }
                if (align) {
                    translateWithProportion(align, outerDockSize, translate, 0, 1);
                    translateWithProportion(align, outerDockSize, translate, 1, 1);
                }
            } else if (align){
                for(let i of [0,1]){
                    translateWithProportion(align, decorations.dock.size[i] ? dockSize : sizeWithoutMargins, translate, i, 1);
                }
            }
        }
        for (let i = 0; i < 2; i++) {
            if (dockSize[i] == true) {
                /* If a true size is used, do a tilde on it in order for the dockhelper to recognize it as true-sized */
                dockSize[i] = ~inUseDockSize[i];
            }
        }
        /* If the renderable is unrenderable due to zero height/width...*/
        if (inUseDockSize[0] === 0 || inUseDockSize[1] === 0) {
            /* Don't display the space if the size is 0*/
            space = 0;
        }
        return {dockSize, translate, innerSize, inUseDockSize, space};
    }

    getDockType(dockMethodToGet) {
        let dockTypes = [['right', 'left'], ['top', 'bottom']];
        return findIndex(dockTypes, (dockMethods) => ~dockMethods.indexOf(dockMethodToGet));
    }

    /**
     * Calculates the bounding box size for all the renderables passed to the function
     * @param {OrderedHashMap} dockedRenderables A map containing Array-pairs of [renderable, renderableCounterpart] containing the things that are attached to the sides.
     * @param {OrderedHashMap} filledRenderables A map containing Array-pairs of [renderable, renderableCounterpart] containing the things that are filled.
     * @param {Object} ownDecorators The decorators that are applied to the view.
     * @returns {Array|Number} The bounding box size of all the renderables
     */
    boundingBoxSize(dockedRenderables, filledRenderables, ownDecorations) {
        let fillSize = [undefined, undefined];
        if (filledRenderables) {
            /* We support having multiple fills */
            fillSize = filledRenderables.reduce((resultingSize, [filledRenderable, renderableCounterpart], renderableName) => {
                this._sizeResolver.settleDecoratedSize(filledRenderable, renderableCounterpart, {size: [NaN, NaN]}, filledRenderable.decorations.size);
                let resolvedSize = this._sizeResolver.getResolvedSize(filledRenderable);
                if (resolvedSize) {
                    for (let [dimension, singleSize] of resolvedSize.entries()) {
                        if (singleSize !== undefined && ((resultingSize[dimension] === undefined) || resultingSize[dimension] < singleSize)) {
                            resultingSize[dimension] = singleSize;
                        }
                    }
                }
                return resultingSize;
            }, [undefined, undefined]);
        }
        let dockSize = [...fillSize];
        if (dockedRenderables) {
            dockSize = this._getDockedRenderablesBoundingBox(dockedRenderables);
            if (fillSize) {
                for (let [dimension, singleFillSize] of fillSize.entries()) {
                    if (singleFillSize !== undefined) {
                        if (dockSize[dimension] === undefined) {
                            dockSize[dimension] = singleFillSize;
                        } else {
                            dockSize[dimension] += singleFillSize;
                        }
                    }
                }
            }
        }

        for (let i = 0; i < 2; i++) {
            if (Number.isNaN(dockSize[i])) {
                dockSize[i] = undefined;
            }
            if (dockSize[i] !== undefined && ownDecorations.viewMargins) {
                let {viewMargins} = ownDecorations;
                /* if i==0 we want margin left and right, if i==1 we want margin top and bottom */
                dockSize[i] += viewMargins[(i + 1) % 4] + viewMargins[(i + 3) % 4];
            }
        }
        return dockSize;
    }

    _getDockedRenderablesBoundingBox(dockedRenderables) {
        let {dockMethod} = dockedRenderables.get(dockedRenderables.keyAt(0))[0].decorations.dock;
        /* Gets the dock type where, 0 is right or left (horizontal) and 1 is top or bottom (vertical) */
        let dockType = this.getDockType(dockMethod);
        let dockingDirection = dockType;
        let orthogonalDirection = !dockType + 0;


        /* Previously countered dock size for docking direction and opposite docking direction */
        let previousDockSize = 0;
        /* Add up the different sizes to if they are docked all in the same direction */
        return dockedRenderables.reduce((result, [dockedRenderable, renderableCounterpart], renderableName) => {
            let {decorations} = dockedRenderable;
            let {dockMethod: otherDockMethod} = decorations.dock;
            /* If docking is done orthogonally */
            if (this.getDockType(otherDockMethod) !== dockType) {
                return [NaN, NaN];
            } else {
                /* Resolve both inner size and outer size */
                this._sizeResolver.settleDecoratedSize(dockedRenderable, renderableCounterpart, {size: [NaN, NaN]}, decorations.dock.size);
                let resolvedOuterSize = this._sizeResolver.getResolvedSize(dockedRenderable);

                let resolvedInnerSize = [undefined, undefined];
                if (dockedRenderable.decorations.size) {
                    this._sizeResolver.settleDecoratedSize(dockedRenderable, renderableCounterpart, {size: [NaN, NaN]}, decorations.size);
                    resolvedInnerSize = this._sizeResolver.getResolvedSize(dockedRenderable);
                }

                if (!resolvedOuterSize || !resolvedInnerSize) {
                    return [NaN, NaN];
                }
                let resolvedSize = [resolvedOuterSize[0] === undefined ? resolvedInnerSize[0] : resolvedOuterSize[0],
                    resolvedOuterSize[1] === undefined ? resolvedInnerSize[1] : resolvedOuterSize[1]];
                let newResult = new Array(2);
                /* If docking is done from opposite directions */
                let dockingFromOpposite = dockMethod !== otherDockMethod;
                if (dockingFromOpposite) {
                    newResult[dockingDirection] = NaN;
                } else {
                    /* If this or the previous renderable size is 0, don't add the space */
                    let spaceSize = (resolvedSize[dockingDirection] === 0 || previousDockSize === 0) ? 0 : decorations.dock.space;
                    newResult[dockingDirection] = resolvedSize[dockingDirection] + spaceSize + result[dockingDirection];
                    previousDockSize = resolvedSize[dockingDirection];
                }
                /* If a size in the orthogonalDirection has been set... */
                if (resolvedSize[orthogonalDirection] !== undefined && !Number.isNaN(resolvedSize[orthogonalDirection])) {
                    /* If there is no result in the orthogonal direction specified yet... */
                    if (result[orthogonalDirection] === undefined) {
                        newResult[orthogonalDirection] = resolvedSize[orthogonalDirection];
                    } else {
                        /* get the max bounding box for the specified orthogonal direction */
                        newResult[orthogonalDirection] = Math.max(result[orthogonalDirection], resolvedSize[orthogonalDirection]);
                    }
                } else {
                    newResult[orthogonalDirection] = result[orthogonalDirection];
                }
                return newResult;
            }
        }, dockingDirection ? [undefined, 0] : [0, undefined]);
    }
}

export class FullSizeLayoutHelper extends BaseLayoutHelper {

    /**
     * Layouts full size renderables
     * @param {OrderedHashMap} A map containing Array-pairs of [renderable, renderableCounterpart] containing the full size renderables.
     * @param {Object} context The famous-flex context with a valid size property
     * @param {Object} ownDecorations. The decorators that are applied to the view.
     */
    layout(fullScreenRenderables, context, ownDecorations) {
        let {extraTranslate} = ownDecorations;
        let names = fullScreenRenderables ? fullScreenRenderables.keys() : [];
        for (let renderableName of names) {
            let [renderable] = fullScreenRenderables.get(renderableName);
            let {callback, transition} = this._getRenderableFlowInformation(renderable);
            let translate = Utils.addTranslations(extraTranslate, renderable.decorations.translate || [0, 0, 0]);
            context.set(renderableName, {
                translate,
                size: context.size,
                opacity: renderable.decorations.opacity === undefined ? 1 : renderable.decorations.opacity,
                callback,
                transition
            });
        }
    }

}

export class TraditionalLayoutHelper extends BaseLayoutHelper {

    layout(traditionalRenderables, context, ownDecorations) {
        let names = traditionalRenderables ? traditionalRenderables.keys() : [];
        for (let renderableName of names) {
            let [renderable, renderableCounterpart] = traditionalRenderables.get(renderableName);
            let renderableSize = this._sizeResolver.settleDecoratedSize(renderable, renderableCounterpart, context, renderable.decorations.size) || [undefined, undefined];
            let {
                translate = [0, 0, 0], origin = [0, 0], align, rotate,
                opacity = 1, scale, skew
            } = renderable.decorations;
            translate = Utils.addTranslations(ownDecorations.extraTranslate, translate);
            let {callback, transition} = this._getRenderableFlowInformation(renderable);
            let adjustedTranslation = Utils.adjustPlacementForTrueSize(renderable, renderableSize, origin, translate, this._sizeResolver);
            context.set(renderableName, {
                size: renderableSize,
                translate: adjustedTranslation,
                origin,
                scale,
                skew,
                align,
                callback,
                transition,
                rotate,
                opacity
            });
        }
    }

    boundingBoxSize(traditionalRenderables) {
        let renderableNames = traditionalRenderables ? traditionalRenderables.keys() : [];
        let totalSize = [undefined, undefined];
        for (let renderableName of renderableNames) {
            let [renderable, renderableCounterpart] = traditionalRenderables.get(renderableName);
            this._sizeResolver.settleDecoratedSize(renderable, renderableCounterpart, {size: [NaN, NaN]}, renderable.decorations.size);
            let size = this._sizeResolver.getResolvedSize(renderable);

            /* Backup: If size can't be resolved, then see if there's a size specified on the decorator */
            if (!size && renderable.decorations) {
                let decoratedSize = renderable.decorations.size;
                let isValidSize = (inputSize) => typeof inputSize == 'number' && inputSize > 0;
                if (decoratedSize && decoratedSize.every(isValidSize)) {
                    size = decoratedSize;
                }
            }
            if (!size) {
                continue;
            }
            let renderableSpec;
            renderableSpec = renderable.decorations;
            let {align = [0, 0]} = renderableSpec;
            let translate = Utils.adjustPlacementForTrueSize(renderable, size, renderableSpec.origin || [0, 0], renderableSpec.translate || [0, 0, 0]);

            /* If there has been an align specified, then nothing can be calculated */
            if (!renderableSpec || !renderableSpec.size || (align[0] && align[1])) {
                continue;
            }

            /* If the renderable has a lower min y/x position, or a higher max y/x position, save its values */
            for (let i = 0; i < 2; i++) {
                /* Undefined is the same as context size */
                if (renderable.decorations.size[i] !== undefined && size[i] !== undefined && !(align && align[i])) {
                    let newPotentialOuterSize = translate[i] + size[i];
                    if (newPotentialOuterSize > totalSize[i] || totalSize[i] === undefined) {
                        totalSize[i] = newPotentialOuterSize;
                    }
                }

            }
        }
        return totalSize;
    }
}