src/utils/view/SizeResolver.js
/**
* Created by lundfall on 01/09/16.
*/
import {limit} from 'arva-js/utils/Limiter.js';
import ImageSurface from 'famous/surfaces/ImageSurface.js';
import AnimationController from 'famous-flex/AnimationController.js';
import {View} from '../../core/View.js';
import {Utils} from './Utils.js';
import EventEmitter from 'eventemitter3';
/**
* Used by the view to keep track of sizes. Emits events to communicate with the view to do certain actions
*/
export class SizeResolver extends EventEmitter {
constructor() {
super();
this._resolvedSizesCache = new Map();
this._trueSizedSurfaceInfo = new Map();
}
/**
* Determines the decorated size. If there is true sizing involved, then it will not return the resolved true size.
* Instead, this can be accessed through getResolvedSize()
* @param {Renderable} renderable. The renderable for which we need the size
* @param {Renderable} renderableCounterpart. The renderable counter-part (e.g. AnimationController, RenderNode, or ContainerSurface).
* @param {Object} context. The context, with a specified size. The size can be set to NaN in order to return NaN
* @param {Array} specifiedSize. The size to use which is specified as a decorator
* @returns {*}
*/
settleDecoratedSize(renderable, renderableCounterpart, context, specifiedSize = [undefined, undefined]) {
let size = [];
let cacheResolvedSize = [];
for (let dimension = 0; dimension < 2; dimension++) {
size[dimension] = this.resolveSingleSize(specifiedSize[dimension], context.size, dimension);
if (this.isValueTrueSized(size[dimension])) {
cacheResolvedSize[dimension] = this._resolveSingleTrueSizedRenderable(renderable, size, dimension, renderableCounterpart);
if (Utils.renderableIsSurface(renderable)) {
size[dimension] = true;
} else {
size[dimension] = cacheResolvedSize[dimension];
}
} else {
size[dimension] = size[dimension] === undefined ? (context.size[dimension] || size[dimension]) : size[dimension];
cacheResolvedSize[dimension] = size[dimension];
}
}
this._resolvedSizesCache.set(renderable, [cacheResolvedSize[0], cacheResolvedSize[1]]);
return (size[0] !== null && size[1] !== null) ? size : null;
}
/**
* Resolves a single dimension (i.e. x or y) size of a renderable.
* @param {Number|Boolean|Object|Undefined|Function} renderableSize Renderable's single dimension size.
* @param {Array.Number} contextSize The context size
* @param {Number} dimension The dimension of the size that is being evaluated (e.g. 1 or 0)
* @returns {Number} The resulting size
* @private
*/
resolveSingleSize(renderableSize, contextSize, dimension) {
switch (typeof renderableSize) {
case 'function':
return this.resolveSingleSize(renderableSize(...contextSize), contextSize, dimension);
case 'number':
/* If 0 < renderableSize < 1, we interpret renderableSize as a fraction of the contextSize */
return renderableSize < 1 && renderableSize > 0 ? renderableSize * Math.max(contextSize[dimension], 0) : renderableSize;
default:
/* renderableSize can be true/false, undefined, or 'auto' for example. */
return renderableSize;
}
}
/**
* Resolves a true size to an actual size of a truesized renderable. size[dim] must be negative or true.
* @param {Renderable} renderable the renderable
* @param {Array} size the size as specified
* @param dim the dimensions e.g. 0,1 that should be processed
* @param {Renderable} renderableCounterpart. The renderable counter-part (e.g. AnimationController, RenderNode, or ContainerSurface).
* @returns {Number} size[dim] will be returned with a non-truesized value
* @private
*/
_resolveSingleTrueSizedRenderable(renderable, size, dim, renderableCounterpart) {
if (size[dim] === -1) {
Utils.warn('-1 detected as set size. If you want a true sized element to take ' +
'up a proportion of your view, please define a function doing so by ' +
'using the context size');
}
/* If there is an AnimationController without content, display 0 size */
if (renderableCounterpart instanceof AnimationController && !renderableCounterpart._showingRenderable) {
return 0;
}
/* True sized element. This has been specified as ~100 where 100 is the initial size
* applying this operator again (e.g. ~~100) gives us the value 100 back
* */
if (Utils.renderableIsComposite(renderable)) {
let twoDimensionalSize = renderable.getSize();
if (!twoDimensionalSize) {
return this._specifyUndeterminedSingleHeight(renderable, size, dim);
} else {
let renderableIsView = renderable instanceof View;
if (size[dim] === true && twoDimensionalSize[dim] === undefined &&
((renderableIsView && (renderable._initialised && !renderable.containsUncalculatedSurfaces())) || !renderableIsView)) {
Utils.warn(`True sized renderable '${renderable.constructor.name}' is taking up the entire context size.`);
return twoDimensionalSize[dim];
} else {
let approximatedSize = size[dim] === true ? twoDimensionalSize[dim] : ~size[dim];
let resultingSize = twoDimensionalSize[dim] !== undefined ? twoDimensionalSize[dim] : approximatedSize;
if (renderableIsView) {
resultingSize = (!renderable.containsUncalculatedSurfaces() && renderable._initialised) ? resultingSize : approximatedSize;
}
return resultingSize;
}
}
} else if (Utils.renderableIsSurface(renderable)) {
let trueSizedSurfaceInfo = this._trueSizedSurfaceInfo.get(renderable) || {};
if (trueSizedSurfaceInfo.calculateOnNext) {
trueSizedSurfaceInfo.calculateOnNext = false;
this._tryCalculateTrueSizedSurface(renderable);
}
let {isUncalculated} = trueSizedSurfaceInfo;
if (isUncalculated === false) {
return trueSizedSurfaceInfo.size[dim];
} else {
if (size[dim] === true) {
let defaultSize = 5;
Utils.warn(`No initial size set for renderable '${renderable.constructor.name}', will default to ${defaultSize}px`);
size[dim] = ~5;
}
if (isUncalculated !== true) {
/* Seems like the surface isn't properly configured, let's get that going */
trueSizedSurfaceInfo = this.configureTrueSizedSurface(renderable);
}
trueSizedSurfaceInfo.trueSizedDimensions[dim] = true;
renderable.size[dim] = true;
/* Need to set the size in order to get resize notifications */
return ~size[dim];
}
} else {
return this._specifyUndeterminedSingleHeight(renderable, size, dim);
}
}
/**
* Determines if the value is true sized
* @param {*} value
* @returns {boolean} True if the value is true sized
* @private
*/
isValueTrueSized(value) {
return value < 0 || value === true
}
_specifyUndeterminedSingleHeight(renderable, size, dim) {
let resultingSize = size[dim] < 0 ? ~size[dim] : 5;
Utils.warn(`Cannot determine size of ${renderable.constructor.name}, falling back to default size or ${resultingSize}px. If the renderable is using legacy declaration this.renderables = ... this isn't supported for true sizing.`);
return resultingSize;
}
containsUncalculatedSurfaces() {
for (let [surface, {isUncalculated}] of this._trueSizedSurfaceInfo) {
if (isUncalculated) {
return true;
}
}
return false;
}
/**
* Calculates a surface size, if possible
* @param renderable
* @private
*/
_tryCalculateTrueSizedSurface(renderable) {
let renderableHtmlElement = renderable._element;
let trueSizedInfo = this._trueSizedSurfaceInfo.get(renderable);
let {trueSizedDimensions} = trueSizedInfo;
if (renderableHtmlElement && ((renderableHtmlElement.offsetWidth && renderableHtmlElement.offsetHeight) || (!renderable.getContent() && !(renderable instanceof ImageSurface))) && renderableHtmlElement.innerHTML === renderable.getContent() &&
(!renderableHtmlElement.style.width || !trueSizedDimensions[0]) && (!renderableHtmlElement.style.height || !trueSizedDimensions[1])) {
let newSize;
newSize = [renderableHtmlElement.offsetWidth, renderableHtmlElement.offsetHeight];
let oldSize = trueSizedInfo.size;
let sizeChange = false;
if (oldSize) {
for (let i = 0; i < 2; i++) {
if (trueSizedDimensions[i] && oldSize[i] !== newSize[i]) {
sizeChange = true;
}
}
} else {
sizeChange = true;
}
if (sizeChange) {
trueSizedInfo.size = newSize;
trueSizedInfo.isUncalculated = false;
}
this.requestRecursiveReflow();
} else {
this.requestReflow();
this.requestLayoutControllerReflow();
}
}
//Todo listen for these in the view
requestRecursiveReflow() {
this.emit('reflowRecursively');
}
requestReflow() {
this.emit('reflow');
}
requestLayoutControllerReflow() {
this.emit('layoutControllerReflow');
}
/**
* Sets up a true sized surface
* @param renderable
* @returns {{isUncalculated: boolean, trueSizedDimensions: boolean[], name: *}} an entry in this._trueSizedSurfaceInfo
* @private
*/
configureTrueSizedSurface(renderable) {
let trueSizedSurfaceInfo = {isUncalculated: true, trueSizedDimensions: [false, false]};
/* We assume both dimensions not to be truesized, they are set in this._resolveDecoratedSize */
this._trueSizedSurfaceInfo.set(renderable, trueSizedSurfaceInfo);
/* Need to set the size in order to get resize notifications */
renderable.size = [undefined, undefined];
renderable.on('resize', () => {
this._tryCalculateTrueSizedSurface(renderable);
});
renderable.on('deploy', () => {
if (!this._trueSizedSurfaceInfo.get(renderable).isUncalculated) {
this._tryCalculateTrueSizedSurface(renderable);
}
});
return trueSizedSurfaceInfo;
}
/**
* Gets the size used when displaying a renderable on the screen the last time the calculation was done.
* @param {Renderable/Name} renderableOrName The renderable or the name of the renderable of which you need the size
*/
getResolvedSize(renderable) {
return this._resolvedSizesCache.get(renderable);
}
doTrueSizedBookkeeping() {
for (let [surface] of this._trueSizedSurfaceInfo) {
/* Encourage the surfaces to check if they have been resized, which could trigger the resize event */
surface._trueSizeCheck = true;
}
}
getSurfaceTrueSizedInfo(surface) {
return this._trueSizedSurfaceInfo.get(surface);
}
}