lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
365 lines • 12.4 kB
JavaScript
import { PropagationModel } from '../events/WidgetEvent.js';
import { Parent } from './Parent.js';
/**
* Makes an iterator for a list of layers, given a starting index and a delta
* for the direction of the iteration. For internal use only.
*
* @internal
*/
function makeLayerIterator(startIndex, delta, layers, layerNames) {
const names = new Map();
for (const [name, nameIndex] of layerNames) {
names.set(nameIndex, name);
}
let index = startIndex;
return {
next() {
var _a;
if (index >= layers.length || index < 0) {
return { value: undefined, done: true };
}
else {
const layer = layers[index];
const name = (_a = names.get(index)) !== null && _a !== void 0 ? _a : null;
index += delta;
return { value: [layer, name], done: false };
}
}
};
}
/**
* A {@link Parent} where each child is in a separate layer. Layers have an
* order, and are placed in that order; layers at the beginning of the list are
* below layers at the end of the list, painting is done in a back-to-front
* order, while event dispatching is done in a front-to-back order.
*
* A layerered container must always contain at least one layer; the default
* layer. The default layer can't be removed, and must be able to expand.
*
* Can be constrained to a specific type of children.
*
* @category Widget
*/
export class LayeredContainer extends Parent {
/**
* @param layers - The list of layers to be added to this container
* @param defaultLayerIndex - The index of the default layer in the layers list
*/
constructor(layers, defaultLayerIndex = 0, properties) {
var _a, _b;
super(properties);
/** The list of layers in this container */
this.layers = new Array();
/**
* A map which names some layers. Each key is the layer name, and each value
* is the index of the layer in the layers list.
*/
this.layerNames = new Map();
const layerCount = layers.length;
if (defaultLayerIndex < 0) {
defaultLayerIndex += layerCount;
}
if (defaultLayerIndex < 0 || defaultLayerIndex >= layerCount) {
throw new Error('Default layer index is out of bounds');
}
for (const layerInit of layers) {
const canExpand = (_a = layerInit.canExpand) !== null && _a !== void 0 ? _a : true;
const name = (_b = layerInit.name) !== null && _b !== void 0 ? _b : null;
const child = layerInit.child;
child.inheritedTheme = this.inheritedTheme;
if (!child) {
throw new Error('A layer must have a child widget');
}
if (name !== null) {
this.layerNames.set(name, this.layers.length);
}
this.layers.push({ child, canExpand });
}
this._defaultLayerIndex = defaultLayerIndex;
this.defaultLayer = this.layers[defaultLayerIndex];
if (!this.defaultLayer.canExpand) {
throw new Error('Default layer must be able to expand the layout');
}
}
/**
* Create a new {@link LayeredContainer} with a single default layer.
* Shortcut for using the constructor with a single-element array as the
* layers list, and a default layer index of 0.
*/
static fromDefaultLayerChild(defaultLayerChild, properties) {
return new LayeredContainer([
{
child: defaultLayerChild,
canExpand: true
}
], 0, properties);
}
*[Symbol.iterator]() {
for (const [layer, _name] of this.backToFrontLayers) {
yield layer.child;
}
}
get childCount() {
return this.layerCount;
}
/**
* Get the amount of layers currently in this container. Equivalent to
* {@link LayeredContainer#childCount};
*/
get layerCount() {
return this.layers.length;
}
/**
* Get the current index of the default layer. May change if a layer is
* inserted or removed before the default layer.
*/
get defaultLayerIndex() {
return this._defaultLayerIndex;
}
/** Iterate all layers from back to front. */
get backToFrontLayers() {
const layers = this.layers;
const layerNames = this.layerNames;
return {
[Symbol.iterator]() {
return makeLayerIterator(0, 1, layers, layerNames);
}
};
}
/** Iterate all layers from front to back. */
get frontToBackLayers() {
const layers = this.layers;
const layerNames = this.layerNames;
return {
[Symbol.iterator]() {
return makeLayerIterator(layers.length - 1, -1, layers, layerNames);
}
};
}
handleEvent(event) {
if (event.propagation !== PropagationModel.Trickling) {
return super.handleEvent(event);
}
// Dispatch event to children. Front layers receive the event before the
// back layers
for (let i = this.layers.length - 1; i >= 0; i--) {
const capturer = this.layers[i].child.dispatchEvent(event);
if (capturer) {
return capturer;
}
}
return null;
}
handlePreLayoutUpdate() {
// Pre-layout update all children. Propagate layoutDirty flag
for (const child of this) {
child.preLayoutUpdate();
// If child's layout is dirty, set self's layout as dirty
if (child.layoutDirty) {
this._layoutDirty = true;
}
}
}
handlePostLayoutUpdate() {
// Post-layout update all children
for (const child of this) {
child.postLayoutUpdate();
}
}
handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight) {
// resolve expanding layers
this.idealWidth = 0;
this.idealHeight = 0;
for (const layer of this.layers) {
if (!layer.canExpand) {
continue;
}
const child = layer.child;
child.resolveDimensions(minWidth, maxWidth, minHeight, maxHeight);
const idealChildDims = child.idealDimensions;
this.idealWidth = Math.max(this.idealWidth, idealChildDims[0]);
this.idealHeight = Math.max(this.idealHeight, idealChildDims[1]);
}
// soft-stretch all layers to fit ideal dimensions
for (const layer of this.layers) {
layer.child.resolveDimensions(this.idealWidth, this.idealWidth, this.idealHeight, this.idealHeight);
}
}
resolvePosition(x, y) {
super.resolvePosition(x, y);
// Resolve children's position to be the same as this widget's position
for (const child of this) {
child.resolvePosition(x, y);
}
}
handlePainting(dirtyRects) {
// Paint children. Back layers are painted before front layers
for (const layer of this.layers) {
layer.child.paint(dirtyRects);
}
}
/** Add a new layer to the container at the end of the layers list. */
pushLayer(layer, name = null) {
this.assertNameAvailable(name);
const index = this.layers.push(layer) - 1;
if (name !== null) {
this.layerNames.set(name, index);
}
this.attachLayer(layer);
return index;
}
/** Add a new layer to the container at a given index of the layers list. */
insertLayerBefore(layer, index, name = null) {
this.assertNameAvailable(name);
const layerCount = this.layers.length;
if (index < 0) {
index = layerCount + index;
if (index < 0) {
index = 0;
}
}
else if (index >= layerCount) {
return this.pushLayer(layer, name);
}
this.updateIndices(index, 1);
this.layers.splice(index, 0, layer);
if (name !== null) {
this.layerNames.set(name, index);
}
this.attachLayer(layer);
return index;
}
/**
* Add a new layer to the container after a given index of the layers list.
*/
insertLayerAfter(layer, index, name = null) {
return this.insertLayerBefore(layer, index + 1, name);
}
/**
* Remove a layer from the container at a given index of the layers list.
*/
removeLayer(index) {
const layerCount = this.layers.length;
if (index < 0) {
index = layerCount + index;
}
if (index < 0 || index >= layerCount) {
throw new RangeError('Cannot remove layer: index out of bounds');
}
if (index === this._defaultLayerIndex) {
throw new Error('Default layer connot be removed');
}
this.detachLayer(this.layers[index]);
this.layers.splice(index, 1);
this.updateIndices(index, -1);
}
/**
* Get the current index of a named layer by its name.
*
* @returns Returns the index of the named layer, or null if no layer has been added with this name.
*/
getNamedLayerIndex(name) {
const index = this.layerNames.get(name);
if (index !== undefined) {
return index;
}
else {
return null;
}
}
/**
* Get a named layer by its name.
*
* @returns Returns the named layer, or null if no layer has been added with this name.
*/
getNamedLayer(name) {
const index = this.layerNames.get(name);
if (index !== undefined) {
return this.layers[index];
}
else {
return null;
}
}
/**
* Get the current index of a layer by its value.
*
* @returns Returns the index of the layer, or null if the layer is not present in the container.
*/
getLayerIndex(layer) {
const index = this.layers.indexOf(layer);
return (index < 0) ? null : index;
}
/**
* Change the indices of the named layers and default layer if they exceed
* indexMin, by a given delta. For internal use only.
*
* @param indexMin - Indices with this value or greater will be updated
* @param delta - The amount of change the indices by
*/
updateIndices(indexMin, delta) {
// XXX is this safe? i've read that it is, but it doesn't feel right
for (const [name, index] of this.layerNames) {
if (index >= indexMin) {
this.layerNames.set(name, index + delta);
}
}
if (this._defaultLayerIndex >= indexMin) {
this._defaultLayerIndex += delta;
}
}
/**
* Attach a given layer to the UI root. For internal use only.
*
* @param layer - The layer to attach to the UI root.
*/
attachLayer(layer) {
const child = layer.child;
if (this.attached) {
child.attach(this._root, this._viewport, this);
}
child.inheritedTheme = this.inheritedTheme;
this._layoutDirty = true;
}
/**
* Detach a given layer from the UI root. For internal use only.
*
* @param layer - The layer to deatach from the UI root.
*/
detachLayer(layer) {
const child = layer.child;
if (this.attached) {
this.markAsDirty([...child.position, ...child.dimensions]);
child.detach();
}
child.inheritedTheme = undefined;
this._layoutDirty = true;
}
/**
* Assert that a layer name is available. If no name is provided, this
* method will always succeed. If the name is already taken, an error is
* thrown.
*/
assertNameAvailable(name) {
if (name !== null && this.layerNames.has(name)) {
throw new Error('');
}
}
}
LayeredContainer.autoXML = {
name: 'layered-container',
inputConfig: [
{
mode: 'layer',
name: 'layers',
list: true
},
{
mode: 'value',
name: 'default-layer-index',
validator: 'number',
optional: true
}
]
};
//# sourceMappingURL=LayeredContainer.js.map