Home Reference Source

src/layout/Decorators.js

/**


 @author: Hans van den Akker (mysim1)
 @license NPOSL-3.0
 @copyright Bizboard, 2016

 */
import merge                    from 'lodash/merge.js';
import extend                   from 'lodash/extend.js';

import AnimationController      from 'famous-flex/AnimationController.js';
import LayoutUtility            from 'famous-flex/LayoutUtility.js';
import Easing                   from 'famous/transitions/Easing.js';

import {Utils}                    from '../utils/view/Utils.js';

function prepDecoratedRenderable(viewOrRenderable, renderableName, descriptor) {
    /* This function can also be called as prepDecoratedRenderable(renderable) */
    if (!renderableName && !descriptor) {
        let renderable = viewOrRenderable;
        renderable.decorations = renderable.decorations || {};
        return renderable;
    }
    let view = viewOrRenderable;

    if (!view.renderableConstructors) {
        view.renderableConstructors = new Map();
    }

    let constructors = view.renderableConstructors;

    /* Because the inherited views share the same prototype, we'll have to split it up depending on which subclass we're referring out */
    let specificRenderableConstructors = constructors.get(view.constructor);
    if (!specificRenderableConstructors) {
        specificRenderableConstructors = constructors.set(view.constructor, {}).get(view.constructor);
    }

    if (!specificRenderableConstructors[renderableName]) {
        /* Getters have a get() method on the descriptor, class properties have an initializer method.
         * get myRenderable(){ return new Surface() } => descriptor.get();
         * myRenderable = new Surface(); => descriptor.initializer();
         */
        if (descriptor.get) {
            specificRenderableConstructors[renderableName] = descriptor.get;
        } else if (descriptor.initializer) {
            specificRenderableConstructors[renderableName] = descriptor.initializer;
        }
    }
    let constructor = specificRenderableConstructors[renderableName];
    if (!constructor.decorations) {
        constructor.decorations = {descriptor: descriptor};
    }

    return constructor;
}

/**
 * Extracts a decorations object
 *
 * @param {View} prototype
 * @returns {Object} The decorations for the prototype
 */
function prepPrototypeDecorations(prototype) {

    /* To prevent inherited classes from taking each others class-level decorators, we need to store these decorations in
     * a map, similarly to function preparing a decorated renderable
     */
    if (!prototype.decorationsMap) {
        prototype.decorationsMap = new Map();
    }

    let {decorationsMap} = prototype;

    let decorations = decorationsMap.get(prototype.constructor);
    if (!decorations) {
        decorations = decorationsMap.set(prototype.constructor, {}).get(prototype.constructor);
    }

    /* Return the class' prototype, so it can be extended by the decorator */
    return decorations;
}

/**
 * Describes a set of decorators used for layouting of a renderable in a View.
 */
export const layout = {


    /**
     * Merely marks a view property as a decorated renderable, which allows it to be rendered.
     * Use this in combination with a @layout.custom decorator on the view in which this renderable resides.
     *
     * @example
     * @layout.renderable
     * renderable = new Surface();
     *
     * @returns {Function} A decorator function
     */
    renderable: function () {
        return function (view, renderableName, descriptor) {
            prepDecoratedRenderable(view, renderableName, descriptor);
        }
    },

    /**
     * Marks the renderable to cover the entire screen. Translate can also be specified on such a renderable.
     *
     * @example
     * @layout.fullSize()
     * // View will have a red background
     * background = new Surface({properties: {backgroundColor: 'red'}});
     *
     * @returns {Function} A decorator function
     */
    fullSize: function () {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.fullSize = true;
        }
    },

    /**
     * Specifies the space that should come before the docked renderable. Useful when not specifying the size in the
     * layout.dock function. Note that the space does not appear if there isn't any renderable with a size greater than
     * zero before it.
     *
     * @example
     * // there's a 20px space before this box
     * @layout.dockSpace(20)
     * @layout.size(100, 100)
     * @layout.dock.left()
     * box = new Surface({properties: {backgroundColor: 'red'}});
     *
     * @param {Number} space The space that is inserted before the renderable.
     * @returns {Function} A decorator function
     */
    dockSpace: function (space) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            // Todo refactor also the z index to the dock
            renderable.decorations.dock = renderable.decorations.dock ? extend(renderable.decorations.dock, {space}) : {space};
        };
    },

    /**
     * Internal function to do docking
     *
     * @param dockMethod
     * @param size
     * @param space
     * @param zIndex
     * @returns {Function}
     */
    _dockTo: function (dockMethod, size, space = 0, zIndex) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);

            if (renderable.decorations.dock) {
                space = space || renderable.decorations.dock.space;
            }

            let width = dockMethod === 'left' || dockMethod === 'right' ? size : undefined;
            let height = dockMethod === 'top' || dockMethod === 'bottom' ? size : undefined;

            let twoDimensionalSize = [width, height];
            // Todo refactor also the z index to the dock, probably
            renderable.decorations.dock = {space, dockMethod, size: twoDimensionalSize};

            if (!renderable.decorations.translate) {
                renderable.decorations.translate = [0, 0, 0];
            }
            if (zIndex) {
                renderable.decorations.translate[2] = zIndex;
            }
        };
    },

    dock: {
        /**
         * Docks the renderable to the left.
         * When using both a docked size and the layout.size decorator, then that layout.size becomes the actual inner size.
         * The renderable can then be stickd within the docking area with origin and align. When combined with align, treats
         * the context size the docking size.
         * When using layout.size without specifying a docked size, it will use that size as docking size. Useful for
         * automatic sizing when parent defines true size and orthogonal size (e.g. height for dock 'left') has to be defined.
         *
         * @example
         * @layout.dock.left(30, 0, 10)
         * @layout.size(15, undefined)
         * @layout.origin(0.5, 0)
         * @layout.align(0.5, 0)
         * dockedRenderable = new Surface({properties: {backgroundColor: 'red'}});
         *
         *
         * @param {Number|Function} [size]. The size of the renderable in the one dimension that is being docked, e.g.
         * dock left or right will be width, whereas dock top or bottom will result in height. For more information about
         * different variations, see layout.size.
         * @param {Number} [space]. Any space that should be inserted before the docked renderable
         * @param {Number} [zIndex]. DEPRECATED: Use translate(0, 0, zIndex) instead.
         * @returns {Function} A decorator function
         */
        left: function () {
            return layout._dockTo('left', ...arguments)
        },

        /**
         * Docks the renderable to the right.
         * When using both a docked size and the layout.size decorator, then that layout.size becomes the actual inner size.
         * The renderable can then be stickd within the docking area with origin and align. When combined with align, treats
         * the context size the docking size.
         * When using layout.size without specifying a docked size, it will use that size as docking size. Useful for
         * automatic sizing when parent defines true size and orthogonal size (e.g. height for dock 'left') has to be defined.
         *
         * @example
         * @layout.dock.right(30, 0, 10)
         * @layout.size(15, undefined)
         * @layout.origin(0.5, 0)
         * @layout.align(0.5, 0)
         * dockedRenderable = new Surface({properties: {backgroundColor: 'red'}});
         *
         * @param {Number|Function} [size]. The size of the renderable in the one dimension that is being docked, e.g.
         * dock left or right will be width, whereas dock top or bottom will result in height. For more information about
         * different variations, see layout.size.
         * @param {Number} [space]. Any space that should be inserted before the docked renderable
         * @param {Number} [zIndex]. DEPRECATED: Use translate(0, 0, zIndex) instead.
         * @returns {Function} A decorator function
         */
        right: function () {
            return layout._dockTo('right', ...arguments)
        },

        /**
         *
         * Docks the renderable to the top.
         * When using both a docked size and the layout.size decorator, then that layout.size becomes the actual inner size.
         * The renderable can then be stickd within the docking area with origin and align. When combined with align, treats
         * the context size the docking size.
         * When using layout.size without specifying a docked size, it will use that size as docking size. Useful for
         * automatic sizing when parent defines true size and orthogonal size (e.g. height for dock 'left') has to be defined.
         *
         * @example
         * @layout.dock.top(30, 0, 10)
         * @layout.size(15, undefined)
         * @layout.origin(0.5, 0)
         * @layout.align(0.5, 0)
         * dockedRenderable = new Surface({properties: {backgroundColor: 'red'}});
         *
         *
         * @param {Number|Function} [size]. The size of the renderable in the one dimension that is being docked, e.g.
         * dock left or right will be width, whereas dock top or bottom will result in height. For more information about
         * different variations, see layout.size.
         * @param {Number} [space = 0]. Any space that should be inserted before the docked renderable
         * @param {Number} [zIndex = 0]. DEPRECATED: Use translate(0, 0, zIndex) instead.
         * @returns {Function} A decorator function
         */
        top: function () {
            return layout._dockTo('top', ...arguments)
        },

        /**
         *
         * Docks the renderable to the bottom.
         * When using both a docked size and the layout.size decorator, then that layout.size becomes the actual inner size.
         * The renderable can then be stickd within the docking area with origin and align. When combined with align, treats
         * the context size the docking size.
         * When using layout.size without specifying a docked size, it will use that size as docking size. Useful for
         * automatic sizing when parent defines true size and orthogonal size (e.g. height for dock 'left') has to be defined.
         *
         * @example
         * @layout.dock.bottom(30, 0, 10)
         * @layout.size(15, undefined)
         * @layout.origin(0.5, 0)
         * @layout.align(0.5, 0)
         * dockedRenderable = new Surface({properties: {backgroundColor: 'red'}});
         *
         *
         * @param {Number|Function} [size]. The size of the renderable in the one dimension that is being docked, e.g.
         * dock left or right will be width, whereas dock top or bottom will result in height. For more information about
         * different variations, see layout.size.
         * @param {Number} [space = 0]. Any space that should be inserted before the docked renderable
         * @param {Number} [zIndex = 0]. DEPRECATED: Use translate(0, 0, zIndex) instead.
         * @returns {Function} A decorator function
         */
        bottom: function () {
            return layout._dockTo('bottom', ...arguments)
        },

        /**
         * Fills the space that is left after the docking with this renderable. When using layout.size, it will use that
         * size as an inner size. This works similarly to other docking, from where translate, size, origin, align, etc
         * can be specified.
         *
         * @example
         * @layout.dock.fill()
         * filledRenderable = new Surface({properties: {backgroundColor: 'red'}});
         *
         * @returns {Function} A decorator function
         */
        fill: function () {
            return layout._dockTo('fill', ...arguments)
        },
        /**
         * Marks the renderable as not being docked anymore. Useful when dynamically changing decorations through
         * this.decorateRenderable or this.setRenderableFlowState
         *
         * @example
         * @layout.dock.fill()
         * @flow.stateStep('nonFilled', layout.dock.none(), layout.size(100, 100))
         * filledRenderable = new Surface({properties: {backgroundColor: 'red'}});
         *
         * @returns {Function} A decorator function
         */
        none: function () {
            return function (view, renderableName, descriptor) {
                let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
                renderable.decorations.disableDock = true;
            }
        }

    },

    /**
     * Makes the renderable allowed to be dragged around. this.renderables[name] refers to a RenderNode containing this
     * draggable along with the renderable itself.
     *
     * @example
     * @layout.draggable({xRange: [0, 100}, yRange: [0, 200]})
     * @layout.size(100, 100)
     * // Makes a draggable square that is red
     * draggableRenderable = new Surface({properties: {backgroundColor: 'red'});
     *
     * @param {Object} [draggableOptions]. Same options that can be passed to a Famous Draggable.
     * @param {Number} [options.snapX] grid width for snapping during drag
     * @param {Number} [options.snapY] grid height for snapping during drag
     * @param {Array.Number} [options.xRange] maxmimum [negative, positive] x displacement from start of drag
     * @param {Array.Number} [options.yRange] maxmimum [negative, positive] y displacement from start of drag
     * @param {Number} [options.scale] one pixel of input motion translates to this many pixels of output drag motion
     * @param {Number} [options.projection] User should set to Draggable._direction.x or
     *    Draggable._direction.y to constrain to one axis.
     * @returns {Function}
     */
    draggable: function (draggableOptions = {}) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.draggableOptions = draggableOptions;
        }
    },

    /**
     * Makes the renderable swipable with physics-like velocity after the dragging is released. Emits event
     * 'thresholdReached' with arguments ('x'|'y', 0|1) when any thresholds have been reached. this.renderables[name]
     * now refers to a a RenderNode containing a positionModifier along with the renderable itself.
     *
     * @example
     * @layout.size(100, 100)
     * @layout.swipable({xRange: [0, 100], snapX: true})
     * //Make a red box that can slide to the right
     * swipable = new Surface({properties: {backgroundColor: 'red'});
     *
     * @param {Object} options
     * @param {Boolean} [options.snapX] Whether to snap to the x axis
     * @param {Boolean} [options.snapY] Whether to snap to the Y axis
     * @param {Boolean} [options.enabled] Whether the swipable should be initially enabled
     * @param {Array.Number} [options.xThreshold] Two values of the thresholds that trigger the thresholdReached event with
     * argument 'x' and second argument 0 or 1, depending on the direction.
     * Specify undefined in one of them to disable threshold to that direction.
     * @param {Array.Number} [options.yThreshold] Two values of the thresholds that trigger the thresholdReached event with
     * argument 'y'  and second argument 0 or 1, depending on the direction.
     * Specify undefined in one of them to disable threshold to that direction.
     * @returns {Function} A decorator function
     */
    swipable: function (options) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.swipableOptions = options;
        }
    },


    /**
     * Clips the renderable by creating another DOM-element with overflow: hidden. Internally, creates a Famous
     * ContainerSurface.
     * The two size parameters can either be a number or undefined (equals the context size).
     *
     * @example
     * @layout.size(40,40)
     * @layout.clip(20, 20)
     * // Shows a quarter of a circle
     * renderable = new Surface({properties: {backgroundColor: 'red', borderRadius: '50%'});
     *
     * @param {Number} width The width of the ContainerSurface
     * @param {Number} heigh The height of the ContainerSurface
     * @param {Object} [properties]. Properties that will be passed to the newly created parent DOM-element.
     * If specified, merged with {overflow: 'hidden'}
     * @returns {Function} A decorator function
     */

    /**
     * Specifies the size of the renderable. For both of the parameters, sizes can be interpreted as follows:
     *
     * If specified as a function, then the argument passed is the context size of the specified dimension
     * (width or height). Note that if an arrow function is used, this scoping cannot be used when inside a
     * decorator, since the scope will be the global scope.
     *
     * If true is specified or a tilde with a size (e.g. ~300), then the renderable will be automatically sized.
     * If a tilde is used to indicate the size, then the size after the tilde will be used when/if the
     * renderable doesn't have a size, or turn into the actual size if it can be determined. This is useful when wanting
     * to reduce the flickering of surfaces who's size cannot be determined the first render tick.
     * Beware that true sizing of surfaces or other raw dom elements (input surfaces, image surfaces, text boxes etc)
     * often comes with a perfomance penalty and should only be used when necessary.
     * Also beware that any negative size will be interpreted as a tilde, since ~x = 1 - x
     *
     * If undefined is specified, then the size of that dimension will equal the entire context size.
     *
     * If a size between 0 and 1 is specified, then that will be interpreted as a proportion of the context size. For
     * example if 0.5 is specified, then the size will be half of the context size (the parent's size). Instead of
     * specifying 1 to cover the entire context size, use undefined instead.
     * @example
     * @layout.size(function(contextWidth) {return Math.max(contextWidth, this.options.maxWidth)}, ~300)
     * // Creates a renderable where the width is equal to the text width and the height is whatever is bigger,
     * // options.maxWidth, or the context size
     * text = new Surface({content: 'This is some text', properties: {backgroundColor: 'red'}});
     *
     * @param {Number|Function} x
     * @param {Number|Function} y
     * @returns {Function} A decorator function
     */
    size: function (x, y) {
        return function (view, renderableName, descriptor) {
            if (Array.isArray(x)) {
                throw Error('Please specify size as two arguments, and not as an array');
            }
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.size = [x, y];
        };
    },
    clip: function (width, height, properties = {}) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.clip = {size: [width, height], properties};
        }
    },

    /**
     * Rotates the renderable around any of the three axes (in radians).
     *
     * @example
     * @layout.size(100,100)
     * @layout.rotate(0, 0, Math.PI)
     * // Writes text upside down
     * renderable = new Surface({content: 'upside down text'});
     *
     * @param {Number} x The rotation around the x axis (flips vertically)
     * @param {Number} y The rotation around the y axis (flips horizontally)
     * @param {Number} z The rotation around the z axis (rotatesin in the more intuitive sense)
     * @returns {Function} A decorator function
     */
    rotate: function (x, y, z) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.rotate = [x, y, z];
        }
    },

    /**
     * Rotates the renderable around any of the three axes (in radians) relatively to the current rotation
     *
     * @example
     * @layout.size(100,100)
     * @layout.rotate(0, 0, Math.PI)
     * // Writes text upside down
     * renderable = new Surface({content: 'upside down text'});
     *
     * @param {Number} x The rotation around the x axis (flips vertically)
     * @param {Number} y The rotation around the y axis (flips horizontally)
     * @param {Number} z The rotation around the z axis (rotatesin in the more intuitive sense)
     * @returns {Function} A decorator function
     */
    rotateFrom: function (x, y, z) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            let propertyName = 'rotate';
            let properties = renderable.decorations[propertyName] || [0,0,0];
            renderable.decorations[propertyName] = [properties[0]+x, properties[1]+y, properties[2]+z];
        }
    },

    /**
     * Sets the opacity of a renderable.
     *
     * @example
     * @layout.opacity(0.5)
     * @layout.size(100, 10)
     * @layout.place.center()
     * // Writes text that is half invisible
     * renderable = new Surface({content: 'Half invisible'});
     *
     * @param {Number} opacity The opacity, between 0 and 1
     * @returns {Function} A decorator function
     */
    opacity: function (opacity) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.opacity = opacity;
        }
    },


    _stickTo: function (stick) {
        return function (view, renderableName, descriptor) {
            let origin = [0, 0], align = [0, 0];
            switch (stick) {
                case 'center':
                    origin = align = [0.5, 0.5];
                    break;
                case 'bottomRight':
                    origin = align = [1, 1];
                    break;
                case 'bottomLeft':
                    origin = align = [0, 1];
                    break;
                case 'topRight':
                    origin = align = [1, 0];
                    break;
                case 'left':
                    origin = align = [0, 0.5];
                    break;
                case 'right':
                    origin = align = [1, 0.5];
                    break;
                case 'top':
                    origin = align = [0.5, 0];
                    break;
                case 'bottom':
                    origin = align = [0.5, 1];
                    break;
                default:
                case 'topLeft':
                    origin = align = [0, 0];
                    break;

            }

            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.origin = origin;
            renderable.decorations.align = align;
        };
    },
    /**
     * Places the renderable by settings origin/align. If nothing is set, it will default to topleft.
     *
     * @example
     * @layout.size(100,~300)
     * @layout.stick.center()
     * renderable = new Surface({content: 'centered text'});
     *
     * @param {String} stick. Can be either of 'center', 'left', 'right', 'bottom', 'top', 'bottomleft', 'bottomright',
     * 'topright', 'topleft'
     * @returns {Function} A decorator function
     */
    stick: {
        center: function () {
            return layout._stickTo('center');
        },
        left: function () {
            return layout._stickTo('left');
        },
        right: function () {
            return layout._stickTo('right');
        },
        top: function () {
            return layout._stickTo('top');
        },
        bottom: function () {
            return layout._stickTo('bottom');
        },
        bottomLeft: function () {
            return layout._stickTo('bottomLeft');
        },
        bottomRight: function () {
            return layout._stickTo('bottomRight');
        },
        topLeft: function () {
            return layout._stickTo('topLeft');
        },
        topRight: function () {
            return layout._stickTo('topRight');
        }
    },

    /**
     * Sets the point where the renderable has its anchor from where rotation and translation will be done.
     * You could consider it as translating the negative of the proportion times its size. The arguments are always
     * between and including 0 and 1.
     *
     * @example
     * @layout.origin(0.5, 0)
     * @layout.align(0.5, 0.5)
     * @layout.size(100,100)
     * //Displays a red box horizontically centered and displays just below the vertical mid point
     * renderable = new Surface({properties: {backgroundColor: 'red'}});
     *
     *
     * @param {Number} x. The x of the origin.
     * @param {Number} y. The y of the origin.
     * @returns {Function} A decorator function.
     */
    origin: function (x, y) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.origin = [x, y];
        };
    },

    /**
     * Translates the renderable by a proportion of the context size.
     *
     * @example
     * @layout.align(0.5, 0.5)
     * @layout.size(100,100)
     * //Displays a red box just below the vertical mid point and past the horizontal mid point
     * renderable = new Surface({properties: {backgroundColor: 'red'}});
     *
     * @param {Number} x. The proportion of the context width that is going to be translated.
     * @param {Number} y. The proportion of the context height that is going to be translated.
     * @returns {Function} A decorator function.
     */
    align: function (x, y) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            renderable.decorations.align = [x, y];
        };
    },

    /**
     * Specifies a translation of a renderable. Can be applied to every kind of renderable (docked, fullSize,
     * and normal). Can also be applied on view level to translate every renderable of that view. The view wide translation defaults
     * to [0, 0, 10] in order to always increase the z space of every level of the Famous rendering tree.
     *
     * @example
     * @layout.translate(0, 0, 20)
     * class myView extends View{
     *  @layout.translate(0, 0, -20)
     *  @layout.fullSize()
     *  // Will display relatively at z level 0 (20 minus 20)
     *  myBackground = new Surface({properties: {backgroudColor: 'red'}});
     * }
     *
     * @param {Number} x Moves the renderable along the x axis.
     * @param {Number} y Moves the renderable along the y axis.
     * @param {Number} z Moves the renderable along the z axis.
     * @returns {Function} A decorator function.
     */
    translate: function (x, y, z) {
        return function (target, renderableName, descriptor) {
            if (Array.isArray(x)) {
                throw Error('Please specify translate as three arguments, and not as an array');
            }
            let propertyName, decorations;
            if (typeof target == 'function') {
                decorations = prepPrototypeDecorations(target.prototype);
                propertyName = 'extraTranslate';
            } else {
                decorations = prepDecoratedRenderable(...arguments).decorations;
                propertyName = 'translate';
            }
            decorations[propertyName] = [x, y, z];
        };
    },

    /**
     * Specifies a relative translation of a renderable. Can be applied to every kind of renderable (docked, fullSize,
     * and normal).
     * Can also be applied on view level to translate every renderable of that view. The view wide translation defaults
     * to [0, 0, 10] in order to always increase the z space of every level of the Famous rendering tree.
     *
     * @example
     * @layout.translateFrom(0, 0, 20)
     * class myView extends View{
     *  @layout.translateFrom(0, 0, -20)
     *  @layout.fullSize()
     *  // Will display relatively at z level 0 (20 minus 20)
     *  myBackground = new Surface({properties: {backgroudColor: 'red'}});
     * }
     *
     * @param {Number} x Moves the renderable along the x axis.
     * @param {Number} y Moves the renderable along the y axis.
     * @param {Number} z Moves the renderable along the z axis.
     * @returns {Function} A decorator function.
     */
    translateFrom: function (x, y, z) {
        return function (target, renderableName, descriptor) {
            if (Array.isArray(x)) {
                throw Error('Please specify translate as three arguments, and not as an array');
            }
            let propertyName, decorations;
            if (typeof target == 'function') {
                decorations = prepPrototypeDecorations(target.prototype);
                propertyName = 'extraTranslate';
            } else {
                decorations = prepDecoratedRenderable(...arguments).decorations;
                propertyName = 'translate';
            }
            let properties = decorations[propertyName] || [0,0,0];
            decorations[propertyName] = [properties[0]+x, properties[1]+y, properties[2]+z];
        };
    },

    /**
     * Specifies the scale of a renderable. Can be applied to every kind of renderable.
     *
     * @example
     *  class myView extends View{
     *  @layout.scale(2, 2, 2)
     *  @layout.fullscreen
     *  // Will scale the renderable by 2 in the x,y,z dimension
     *  myBackground = new Surface({properties: {backgroudColor: 'red'}});
     * }
     *
     * @param {Number} x Scales the renderable along the x axis.
     * @param {Number} y Scales the renderable along the y axis.
     * @param {Number} z Scales the renderable along the z axis.
     * @returns {Function} A decorator function.
     */
    scale: function (x,
                     y = Utils.warn('Please specify y parameter for scaling'),
                     z = Utils.warn('Please specify z parameter for scaling')) {
        return function (target, renderableName, descriptor) {
            let decorations = prepDecoratedRenderable(...arguments).decorations;
            let propertyName = 'scale';
            decorations[propertyName] = [x, y, z];
        };
    },

    /**
     * Specifies the skew of a renderable. Can be applied to every kind of renderable.
     *
     * @example
     *  class myView extends View{
     *  @layout.skew(2, 2, 2)
     *  @layout.fullscreen
     *  // Will skew the renderable by 2 in the x,y,z dimension
     *  myBackground = new Surface({properties: {backgroudColor: 'red'}});
     * }
     *
     * @param {Number} x Skews the renderable along the x axis.
     * @param {Number} y Skews the renderable along the y axis.
     * @param {Number} z Skews the renderable along the z axis.
     * @returns {Function} A decorator function.
     */
    skew: function (x, y, z) {
        return function (target, renderableName, descriptor) {
            let decorations = prepDecoratedRenderable(...arguments).decorations;
            let propertyName = 'skew';
            decorations[propertyName] = [x, y, z];
        };
    },

    /**
     *
     * Creates an animation controller to show/hide the renderable. Renderables can be shown by calling
     * this.showRenderable(renderableName) and hidden using this.hideRenderable(renderableName) or
     * this.showRenderable(renderableName, false). When a renderable has been shown, it will emit the event 'shown'.
     *
     * @example
     * @layout.stick.center()
     * @layout.size(100,100)
     * @layout.animate({transition: {duration: 350}})
     * renderable = new Surface({properties: {backgroundColor: 'red'}});
     *
     *
     *
     * @param {Object} [options] The same as famous-flex Animation Controller, plus 2 more:
     * @param {Boolean} [options.showInitially] Whether to show the renderable when the view is created. (Default: true).
     * @param {String} [options.waitFor] If specified, it will wait for the renderable with the specified name to show
     * before showing the renderable
     * @param {Object} [options.transition] Transition options.
     * @param {Function} [options.animation] Animation function (default: `AnimationController.Animation.FadedZoom`).
     * @param {Number} [options.zIndexOffset] Optional z-index difference between the hiding & showing renderable (default: 0).
     * @param {Number} [options.keepHiddenViewsInDOMCount] Keeps views in the DOM after they have been hidden (default: 0).
     * @param {Object} [options.show] Show specific options.
     * @param {Object} [options.show.transition] Show specific transition options.
     * @param {Function} [options.show.animation] Show specific animation function.
     * @param {Object} [options.hide] Hide specific options.
     * @param {Object} [options.hide.transition] Hide specific transition options.
     * @param {Function} [options.hide.animation] Hide specific animation function.
     * @param {Object} [options.transfer] Transfer options.
     * @param {Object} [options.transfer.transition] Transfer specific transition options.
     * @param {Number} [options.transfer.zIndex] Z-index the tranferables are moved on top while animating (default: 10).
     * @param {Bool} [options.transfer.fastResize] When enabled, scales the renderable i.s.o. resizing when doing the transfer animation (default: true).
     * @param {Array} [options.transfer.items] Ids (key/value) pairs (source-id/target-id) of the renderables that should be transferred.
     * @returns {Function}
     */
    animate: function (options = {}) {
        return function (view, renderableName, descriptor) {
            let renderableConstructor = prepDecoratedRenderable(view, renderableName, descriptor);
            options = merge({
                showInitially: true,
                animation: AnimationController.Animation.FadedZoom,
                show: {transition: options.transition || {curve: Easing.outCubic, duration: 250}},
                hide: {transition: options.transition || {curve: Easing.inCubic, duration: 250}}
            }, options);

            renderableConstructor.decorations.animation = options;

            constructor.decorations = renderableConstructor.decorations;

        };
    },

    /**
     * Makes the view flow by tweening all intermediate stages of a changed attribute of any renderable.
     *
     * @example
     * @layout.flow({spring: {dampingRatio: 0.8, period: 1000}})
     * class myView extends View{
     * ...
     * }
     *
     * @param {Object} Options to pass as flowOptions to the LayoutController
     * @param {Bool} [flowOptions.transition] If specified, sets the default transition to use
     * @param {Bool} [flowOptions.reflowOnResize] Smoothly reflows renderables on resize (only used when flow = true) (default: `true`).
     * @param {Object} [flowOptions.spring] Spring options used by nodes when reflowing (default: `{dampingRatio: 0.8, period: 300}`).
     * @param {Object} [flowOptions.properties] Properties which should be enabled or disabled for flowing.
     * @param {Spec} [flowOptions.insertSpec] Size, transform, opacity... to use when inserting new renderables into the scene (default: `{}`).
     * @param {Spec} [flowOptions.removeSpec] Size, transform, opacity... to use when removing renderables from the scene (default: undefined).
     * @returns {Function} A decorator function
     */
    flow: function (flowOptions = {}) {
        return function (target) {
            let decorations = prepPrototypeDecorations(target.prototype);
            decorations.useFlow = true;
            decorations.flowOptions = flowOptions || {};
            decorations.transition = flowOptions.transition || undefined;
        }
    },

    /**
     * Makes the view as scrollable. This will put the entire content in a ReflowingScrollView that uses getSize on the
     * view to determine scrolling size. If the size cannot be determined, you might consider declaring your own
     * getSize() on the View.
     *
     * @example
     * @layout.scrollable()
     * class myView extends View{
     * ...
     * }
     *
     *
     * @returns {Function} A decorator function
     */

    scrollable: function (options = {}) {
        return function (target) {
            let decorations = prepPrototypeDecorations(target.prototype);
            decorations.scrollableOptions = options;
        }
    },

    /**
     * Experimental feature of scrolling natively.
     *
     * @param {Object} [options] Options on how to scroll
     * @param {Boolean} [options.scrollY] Defaults to true
     * @param {Boolean} [options.scrollX] Defaults to false
     * @returns {Function} A decorator function
     */
    nativeScrollable: function (options = {}) {
        let {scrollY = true, scrollX= false} = options;
        return function (target) {
            let decorations = prepPrototypeDecorations(target.prototype);
            decorations.nativeScrollable = {scrollY, scrollX};
        }
    },

    /**
     * Sets the margins for the docked content. This can be applied both to a child and a class. When in conflict,
     * the parent will override the child's setting. If the margin is set on a Surface, then CSS padding will be set.
     * margins can be 1, 2, or 4, parameters, which can be specified as shorthand in the same way
     * as CSS does it.
     *
     * @example
     * @layout.dockPadding(15)
     * //Creates a class with 15px margin on all sides for docked renderables
     * class myView extends View{
     *
     *  //Will be displayed with margin
     *  @layout.dock.top(20)
     *  onTop = new Surface({content: "hello world"});
     *
     *  //Will be displayed without margin since we're using @layout.stick
     *  @layout.stick.bottom
     *  onButtom = new Surface({content: "hey hey"});
     * }
     *

     *
     * @param {Number} firstMargin
     * @param {Number} [secondMargin]
     * @param {Number} [thirdMargin]
     * @param {Number} [fourthMargin]
     * @returns {Function} A decorator function
     */
    dockPadding: function (...margins) {
        return function (target) {
            let decorations;
            if (typeof target == 'function') {
                decorations = prepPrototypeDecorations(target.prototype);
            } else {
                decorations = prepDecoratedRenderable(...arguments).decorations;
            }
            decorations.viewMargins = LayoutUtility.normalizeMargins(margins);
        };
    },

    /**
     * Like @layout.dockPadding, sets the padding between this view and its docked content.
     * When the screen width plus this padding exceeds maxContentWidth, the padding
     * is increased, so that the content is never wider than maxContentWidth.
     *
     * @example
     * @layout.columnDockPadding(720, [16])
     * //Creates a class with 16px margin on all sides for docked renderables
     * class myView extends View{
     *
     *  //Will be displayed with margin to the top and sides, and will at max be 720px wide.
     *  @layout.dock.top(20)
     *  onTop = new Surface({content: "hello world"});
     *
     *  //Will be displayed without margin since we're using @layout.stick instead of @layout.dock
     *  @layout.stick.bottom
     *  onButtom = new Surface({content: "hey hey"});
     * }
     *
     * @param {Number} maxContentWidth Maximum width the content should be allowed to be.
     * @param {Array.Number} defaultPadding A 1-D, 2-D, or 4-D array of padding numbers, just like the padding spec in CSS.
     * @returns {Function}
     */
    columnDockPadding: function (maxContentWidth = 720, defaultPadding = [0, 16, 0, 16]) {
        return function (target) {
            let decorations = prepPrototypeDecorations(target.prototype);
            let normalisedPadding = LayoutUtility.normalizeMargins(defaultPadding);

            /* Default to 16px dockPadding */
            layout.dockPadding(normalisedPadding);

            /* Calculate the dockPadding dynamically every time the View's size changes.
             * The results from calling this method are further handled in View.js.
             *
             * The logic behind this is 16px padding by default, unless the screen is
             * wider than 720px. In that case, the padding is increased to make the content
             * in between be at maximum 720px. */
            decorations.dynamicDockPadding = function(size) {
                let sideWidth = size[0] > maxContentWidth + 32 ? (size[0] - maxContentWidth) / 2 : normalisedPadding[1];
                return [normalisedPadding[0], sideWidth, normalisedPadding[2], sideWidth];
            }
        };
    },

    /**
     *
     * Adds a custom layout function to the view.
     * This decorator works directly on the object so you shouldn't pass any arguments nor use parentheses.
     *
     * @example
     * @layout.custom((context) => {
     *  context.set('myRenderable', {
     *  size: [100, 100]
     * })
     * class MyView extends View {
     *  constructor(options) {
     *      super(options);
     *      this.renderables.myRenderable = new Surface({properties: {backgroundColor: 'red'}});
     *  }
     * }
     *
     *
     * @param customLayoutFunction
     * @returns {Function} A decorator function
     */
    custom: function (customLayoutFunction) {
        return function (target) {
            let decorations = prepPrototypeDecorations(target.prototype);
            decorations.customLayoutFunction = customLayoutFunction;
        };
    }
};

export const event = {
    /**
     * Internal function used by the event decorators to generalize the idea of on, once, and off.
     *
     * @param {String} subscriptionType A type of subscription function, e.g. on
     * @param {String} eventName The event name
     * @param {Function} callback that is called when event has happened
     * @returns {Function}
     */
    _subscribe: function (subscriptionType, eventName, callback) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            if (!renderable.decorations.eventSubscriptions) {
                renderable.decorations.eventSubscriptions = [];
            }
            renderable.decorations.eventSubscriptions.push({
                subscriptionType: subscriptionType,
                eventName: eventName,
                callback: callback
            });
        };
    },

    /**
     *
     * Adds an event listener to the renderable when specific event happened.
     *
     * @example
     * @layout.on('click', function() {this._handleClick})
     * thing = new Surface({properties: {backgroundColor: 'red'}});
     *
     * _handleClick() { ... }
     *
     *
     * @param eventName
     * @param callback
     * @returns {Function} A decorator function
     */
    on: function (eventName, callback) {
        return event._subscribe('on', eventName, callback);
    },


    /**
     *
     * Adds an event listener to the renderable when specific event happened once.
     *
     * @example
     * @layout.size(100,100)
     * @layout.stick.center()
     * @layout.once('click', function() {this._handleClick})
     * thing = new Surface({properties: {backgroundColor: 'red'}});
     *
     * _handleClick() { ... }
     *
     *
     * @param eventName
     * @param callback
     * @returns {Function} A decorator function
     */
    once: function (eventName, callback) {
        return event._subscribe('once', eventName, callback);
    },

    /**
     * Pipes events from one renderable to another. The other renderable has to be declared above the one that is doing
     * the piping, otherwise an exception will be thrown.
     *
     * @example
     * @layout.fullSize()
     * @layout.pipe('dbsv')
     * //Pipe events to another renderable declared above, called 'dbsv'
     * scrollableSurface = new Surface();
     *
     * @param pipeToName
     * @returns {Function}
     */
    pipe: function (pipeToName) {
        return function (view, renderableName, descriptor) {
            let renderable = prepDecoratedRenderable(view, renderableName, descriptor);
            if (!renderable.decorations.pipes) {
                renderable.decorations.pipes = [];
            }

            renderable.decorations.pipes.push(pipeToName);
        };
    }
};

export const flow = {
    /**
     * Sets the default flow options for a View. These options will be overridden by
     * each of its renderables, if they have flow options defined through e.g. flow.stateStep()
     *
     * @example
     * @flow.defaultOptions({ transition: { curve: Easing.outCubic, duration: 200 } })
     * class MyView extends View {
     * }
     *
     * @param {Object} flowOptions Options to set as default.
     * @param {Object} [flowOptions.delay] The amount of milliseconds to wait in between state transitions.
     * @param {Object} [flowOptions.transition] A Famo.us-compatible transition object defining the animation specifics.
     * @param {Object} [flowOptions.transition.curve] The animation curve to use when flowing from one state to another, e.g. Easing.outCubic.
     * @param {Object} [flowOptions.transition.duration] The amount of milliseconds a flow animation should take.
     * @returns {Function}
     */
    defaultOptions: function (flowOptions = {}) {
        return function (target, renderableName, descriptor) {
            let decorations = prepDecoratedRenderable(...arguments).decorations;
            if (!decorations.flow) {
                decorations.flow = {states: {}};
            }
            decorations.flow.defaults = {...flowOptions};
        }
    },

    /**
     * Functions the same as @flow.stateStep(), and additionally also immediately applies the decorators passed into the 'transformations' argument.
     * Used to define a state step, without having to also manually apply the same decorators to the renderable to ensure it is rendered this way
     * on initial show.
     *
     * @example
     * // Initial size is [100, 100], and rendered at center of parent.
     * @flow.defaultState('active', {}, layout.size(100, 100), layout.stick.center())
     * myRenderable = new Surface();
     *
     * @param {String} stateName The state name to assign to this state step.
     * @param {Object} [stateOptions] Flow options to use in the state step.
     * @param {Object} [stateOptions.delay] The amount of milliseconds to wait in between state transitions.
     * @param {Object} [stateOptions.transition] A Famo.us-compatible transition object defining the animation specifics.
     * @param {Object} [stateOptions.transition.curve] The animation curve to use when flowing from one state to another, e.g. Easing.outCubic.
     * @param {Object} [stateOptions.transition.duration] The amount of milliseconds a flow animation should take.
     * @param {Array.Function} transformations Decorators to assign to this state, and to apply initially, passed in as regular comma-separated arguments.
     * @returns {Function}
     */
    defaultState: function (stateName = '', stateOptions = {}, ...transformations) {
        return function (target, renderableName, descriptor) {
            flow.stateStep(stateName, stateOptions, ...transformations)(target, renderableName, descriptor);
            for(let transformation of transformations) {
                transformation(target, renderableName, descriptor);
            }
        }
    },

    /**
     * Used to define a state that the renderable is able to flow to. When multiple state steps with the same state name
     * are defined, flowing into that state will sequentially execute all defined steps with that state name.
     *
     * @example
     * // Initial size is [0, 0], and rendered at top left of parent, because no @flow.defaultStep() was done,
     * // and no other decorators are applied to the renderable.
     * @flow.stateStep('active', {}, layout.size(100, 100), layout.stick.center())
     * myRenderable = new Surface();
     *
     * @param {String} stateName The state name to assign to this state step.
     * @param {Object} [stateOptions] Flow options to use in the state step.
     * @param {Object} [stateOptions.delay] The amount of milliseconds to wait in between state transitions.
     * @param {Object} [stateOptions.transition] A Famo.us-compatible transition object defining the animation specifics.
     * @param {Object} [stateOptions.transition.curve] The animation curve to use when flowing from one state to another, e.g. Easing.outCubic.
     * @param {Object} [stateOptions.transition.duration] The amount of milliseconds a flow animation should take.
     * @param {Array.Function} transformations Decorators to assign to this state, and to apply initially, passed in as regular comma-separated arguments.
     * @returns {Function}
     */
    stateStep: function (stateName = '', stateOptions = {}, ...transformations) {
        return function (target, renderableName, descriptor) {
            let decorations = prepDecoratedRenderable(...arguments).decorations;
            if (!decorations.flow) {
                decorations.flow = {states: {}};
            }
            if (!decorations.flow.states[stateName]) {
                decorations.flow.states[stateName] = {steps: []};
            }
            decorations.flow.states[stateName].steps.unshift({transformations, options: stateOptions});
        }
    },

    /**
     * Defines the View-level states, that exist of concurrently and sequentially executed renderable-level states.
     * When e.g. View.setViewFlowState('active') is called, the renderable states defined in the view-level state 'active' are executed.
     *
     * @example
     * // Calling setViewFlowState('active') will first hide the loader, and when that is completed, show both buttons at the same time.
     * @flow.viewStates({ 'active': [{loader: 'hidden'}, { button1: 'active', button2: 'active' }] })
     * class MyView extends View {
     *
     *   @flow.defaultState('shown', {}, layout.opacity(1), layout.fullSize())
     *   @flow.stateStep('hidden', {}, layout.opacity(0))
     *   loader = new Surface();
     *
     *   @flow.defaultState('inactive', {}, layout.opacity(0), layout.size(100, 100), layout.stick.top())
     *   @flow.stateStep('active', {}, layout.opacity(1))
     *   button1 = new Surface();
     *
     *   @flow.defaultState('inactive', {}, layout.opacity(0), layout.size(100, 100), layout.stick.bottom())
     *   @flow.stateStep('active', {}, layout.opacity(1))
     *   button1 = new Surface();
     * }
     *
     * @param {Object} states An object keyed by View-level state names, with values of arrays of objects.
     * @returns {Function}
     */
    viewStates: function (states = {}) {
        return function (target) {
            let decorations = prepPrototypeDecorations(target.prototype);
            if (!decorations.flow) {
                decorations.flow = {};
            }
            decorations.flow.viewStates = states;
        }
    },

    /**
     * A wrapper around @flow.stateStep, to allow defining multiple steps with the same state name.
     *
     * @param {String} stateName State name to assign states to.
     * @param {Array.Object} states An array of {stateOptions: [..], transformations: [..]} objects, with stateOptions and transformations
     * being the same usage as @flow.stateStep().
     * @returns {Function}
     */
    multipleStateStep: function(stateName = '', states = []){
        return function (target, renderableName, descriptor) {
            for(let {stateOptions, transformations} of states){
                flow.stateStep(stateName, stateOptions, ...transformations)(target, renderableName, descriptor);
            }
        }
    }
};