UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

365 lines 12.4 kB
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