@babylonjs/gui
Version:
Babylon.js GUI module =====================
616 lines • 22.4 kB
JavaScript
import { __decorate } from "@babylonjs/core/tslib.es6.js";
import { Logger } from "@babylonjs/core/Misc/logger.js";
import { Control } from "./control.js";
import { Measure } from "../measure.js";
import { RegisterClass } from "@babylonjs/core/Misc/typeStore.js";
import { serialize } from "@babylonjs/core/Misc/decorators.js";
import { DynamicTexture } from "@babylonjs/core/Materials/Textures/dynamicTexture.js";
import { Texture } from "@babylonjs/core/Materials/Textures/texture.js";
import { Constants } from "@babylonjs/core/Engines/constants.js";
import { Observable } from "@babylonjs/core/Misc/observable.js";
import { Tools } from "@babylonjs/core/Misc/tools.js";
import { Matrix2D } from "../math2D.js";
/**
* Root class for 2D containers
* @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#containers
*/
export class Container extends Control {
/** Gets or sets boolean indicating if children should be rendered to an intermediate texture rather than directly to host, useful for alpha blending */
get renderToIntermediateTexture() {
return this._renderToIntermediateTexture;
}
set renderToIntermediateTexture(value) {
if (this._renderToIntermediateTexture === value) {
return;
}
this._renderToIntermediateTexture = value;
this._markAsDirty();
}
/** Gets or sets a boolean indicating if the container should try to adapt to its children height */
get adaptHeightToChildren() {
return this._adaptHeightToChildren;
}
set adaptHeightToChildren(value) {
if (this._adaptHeightToChildren === value) {
return;
}
this._adaptHeightToChildren = value;
if (value) {
this.height = "100%";
}
this._markAsDirty();
}
/** Gets or sets a boolean indicating if the container should try to adapt to its children width */
get adaptWidthToChildren() {
return this._adaptWidthToChildren;
}
set adaptWidthToChildren(value) {
if (this._adaptWidthToChildren === value) {
return;
}
this._adaptWidthToChildren = value;
if (value) {
this.width = "100%";
}
this._markAsDirty();
}
/** Gets or sets background color */
get background() {
return this._background;
}
set background(value) {
if (this._background === value) {
return;
}
this._background = value;
this._markAsDirty();
}
/** Gets or sets background gradient color. Takes precedence over background */
get backgroundGradient() {
return this._backgroundGradient;
}
set backgroundGradient(value) {
if (this._backgroundGradient === value) {
return;
}
this._backgroundGradient = value;
this._markAsDirty();
}
/** Gets the list of children */
get children() {
return this._children;
}
get isReadOnly() {
return this._isReadOnly;
}
set isReadOnly(value) {
this._isReadOnly = value;
for (const child of this._children) {
child.isReadOnly = value;
}
}
/**
* Creates a new Container
* @param name defines the name of the container
*/
constructor(name) {
super(name);
this.name = name;
/** @internal */
this._children = new Array();
/** @internal */
this._measureForChildren = Measure.Empty();
/** @internal */
this._background = "";
/** @internal */
this._backgroundGradient = null;
/** @internal */
this._adaptWidthToChildren = false;
/** @internal */
this._adaptHeightToChildren = false;
/** @internal */
this._renderToIntermediateTexture = false;
/** @internal */
this._intermediateTexture = null;
/**
* Gets or sets a boolean indicating that the container will let internal controls handle picking instead of doing it directly using its bounding info
*/
this.delegatePickingToChildren = false;
/**
* Gets or sets a boolean indicating that layout cycle errors should be displayed on the console
*/
this.logLayoutCycleErrors = false;
/**
* Gets or sets the number of layout cycles (a change involved by a control while evaluating the layout) allowed
*/
this.maxLayoutCycle = 3;
/**
* An event triggered when any control is added to this container.
*/
this.onControlAddedObservable = new Observable();
/**
* An event triggered when any control is removed from this container.
*/
this.onControlRemovedObservable = new Observable();
this._inverseTransformMatrix = Matrix2D.Identity();
this._inverseMeasure = new Measure(0, 0, 0, 0);
}
_getTypeName() {
return "Container";
}
_flagDescendantsAsMatrixDirty() {
for (const child of this.children) {
child._isClipped = false;
child._markMatrixAsDirty();
}
}
/**
* Gets a child using its name
* @param name defines the child name to look for
* @returns the child control if found
*/
getChildByName(name) {
for (const child of this.children) {
if (child.name === name) {
return child;
}
}
return null;
}
/**
* Gets a child using its type and its name
* @param name defines the child name to look for
* @param type defines the child type to look for
* @returns the child control if found
*/
getChildByType(name, type) {
for (const child of this.children) {
if (child.typeName === type) {
return child;
}
}
return null;
}
/**
* Search for a specific control in children
* @param control defines the control to look for
* @returns true if the control is in child list
*/
containsControl(control) {
return this.children.indexOf(control) !== -1;
}
/**
* Adds a new control to the current container
* @param control defines the control to add
* @returns the current container
*/
addControl(control) {
if (!control) {
return this;
}
const index = this._children.indexOf(control);
if (index !== -1) {
return this;
}
control._link(this._host);
control._markAllAsDirty();
this._reOrderControl(control);
this._markAsDirty();
this.onControlAddedObservable.notifyObservers(control);
return this;
}
/**
* Removes all controls from the current container
* @returns the current container
*/
clearControls() {
const children = this.children.slice();
for (const child of children) {
this.removeControl(child);
}
return this;
}
/**
* Removes a control from the current container
* @param control defines the control to remove
* @returns the current container
*/
removeControl(control) {
const index = this._children.indexOf(control);
if (index !== -1) {
this._children.splice(index, 1);
control.parent = null;
}
control.linkWithMesh(null);
if (this._host) {
this._host._cleanControlAfterRemoval(control);
}
this._markAsDirty();
this.onControlRemovedObservable.notifyObservers(control);
return this;
}
/**
* @internal
*/
_reOrderControl(control) {
const linkedMesh = control.linkedMesh;
this.removeControl(control);
let wasAdded = false;
for (let index = 0; index < this._children.length; index++) {
if (this._children[index].zIndex > control.zIndex) {
this._children.splice(index, 0, control);
wasAdded = true;
break;
}
}
if (!wasAdded) {
this._children.push(control);
}
control.parent = this;
if (linkedMesh) {
control.linkWithMesh(linkedMesh);
}
this._markAsDirty();
}
/**
* @internal
*/
_offsetLeft(offset) {
super._offsetLeft(offset);
for (const child of this._children) {
child._offsetLeft(offset);
}
}
/**
* @internal
*/
_offsetTop(offset) {
super._offsetTop(offset);
for (const child of this._children) {
child._offsetTop(offset);
}
}
/** @internal */
_markAllAsDirty() {
super._markAllAsDirty();
for (let index = 0; index < this._children.length; index++) {
this._children[index]._markAllAsDirty();
}
}
_getBackgroundColor(context) {
return this._backgroundGradient ? this._backgroundGradient.getCanvasGradient(context) : this._background;
}
/**
* @internal
*/
_localDraw(context) {
if (this._background || this._backgroundGradient) {
context.save();
if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
context.shadowColor = this.shadowColor;
context.shadowBlur = this.shadowBlur;
context.shadowOffsetX = this.shadowOffsetX;
context.shadowOffsetY = this.shadowOffsetY;
}
context.fillStyle = this._getBackgroundColor(context);
context.fillRect(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
context.restore();
}
}
/**
* @internal
*/
_link(host) {
super._link(host);
for (const child of this._children) {
child._link(host);
}
}
/** @internal */
_beforeLayout() {
// Do nothing
}
/**
* @internal
*/
_processMeasures(parentMeasure, context) {
if (this._isDirty || !this._cachedParentMeasure.isEqualsTo(parentMeasure)) {
super._processMeasures(parentMeasure, context);
this._evaluateClippingState(parentMeasure);
if (this._renderToIntermediateTexture) {
if (this._intermediateTexture && this._host.getScene() != this._intermediateTexture.getScene()) {
this._intermediateTexture.dispose();
this._intermediateTexture = null;
}
if (!this._intermediateTexture) {
this._intermediateTexture = new DynamicTexture("", { width: this._currentMeasure.width, height: this._currentMeasure.height }, this._host.getScene(), false, Texture.NEAREST_SAMPLINGMODE, Constants.TEXTUREFORMAT_RGBA, false);
this._intermediateTexture.hasAlpha = true;
}
else {
this._intermediateTexture.scaleTo(this._currentMeasure.width, this._currentMeasure.height);
}
}
}
}
/**
* @internal
*/
_layout(parentMeasure, context) {
if (!this.isDirty && (!this.isVisible || this.notRenderable)) {
return false;
}
this.host._numLayoutCalls++;
if (this._isDirty) {
this._currentMeasure.transformToRef(this._transformMatrix, this._prevCurrentMeasureTransformedIntoGlobalSpace);
}
let rebuildCount = 0;
context.save();
this._applyStates(context);
this._beforeLayout();
do {
let computedWidth = -1;
let computedHeight = -1;
this._rebuildLayout = false;
this._processMeasures(parentMeasure, context);
if (!this._isClipped) {
for (const child of this._children) {
child._tempParentMeasure.copyFrom(this._measureForChildren);
if (child._layout(this._measureForChildren, context)) {
if (child.isVisible && !child.notRenderable) {
if (this.adaptWidthToChildren && child._width.isPixel) {
computedWidth = Math.max(computedWidth, child._currentMeasure.width + child._paddingLeftInPixels + child._paddingRightInPixels);
}
if (this.adaptHeightToChildren && child._height.isPixel) {
computedHeight = Math.max(computedHeight, child._currentMeasure.height + child._paddingTopInPixels + child._paddingBottomInPixels);
}
}
}
}
if (this.adaptWidthToChildren && computedWidth >= 0) {
computedWidth += this.paddingLeftInPixels + this.paddingRightInPixels;
const width = computedWidth + "px";
if (this.width !== width) {
this.parent?._markAsDirty();
this.width = width;
this._width.ignoreAdaptiveScaling = true;
this._rebuildLayout = true;
}
}
if (this.adaptHeightToChildren && computedHeight >= 0) {
computedHeight += this.paddingTopInPixels + this.paddingBottomInPixels;
const height = computedHeight + "px";
if (this.height !== height) {
this.parent?._markAsDirty();
this.height = height;
this._height.ignoreAdaptiveScaling = true;
this._rebuildLayout = true;
}
}
this._postMeasure();
}
rebuildCount++;
} while (this._rebuildLayout && rebuildCount < this.maxLayoutCycle);
if (rebuildCount >= 3 && this.logLayoutCycleErrors) {
Logger.Error(`Layout cycle detected in GUI (Container name=${this.name}, uniqueId=${this.uniqueId})`);
}
context.restore();
if (this._isDirty) {
this.invalidateRect();
this._isDirty = false;
}
return true;
}
_postMeasure() {
// Do nothing by default
}
/**
* @internal
*/
_draw(context, invalidatedRectangle) {
const renderToIntermediateTextureThisDraw = this._renderToIntermediateTexture && this._intermediateTexture;
const contextToDrawTo = renderToIntermediateTextureThisDraw ? this._intermediateTexture.getContext() : context;
if (renderToIntermediateTextureThisDraw) {
contextToDrawTo.save();
contextToDrawTo.translate(-this._currentMeasure.left, -this._currentMeasure.top);
if (invalidatedRectangle) {
this._transformMatrix.invertToRef(this._inverseTransformMatrix);
invalidatedRectangle.transformToRef(this._inverseTransformMatrix, this._inverseMeasure);
contextToDrawTo.clearRect(this._inverseMeasure.left, this._inverseMeasure.top, this._inverseMeasure.width, this._inverseMeasure.height);
}
else {
contextToDrawTo.clearRect(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
}
}
this._localDraw(contextToDrawTo);
context.save();
if (this.clipChildren) {
this._clipForChildren(contextToDrawTo);
}
for (const child of this._children) {
// Only redraw parts of the screen that are invalidated
if (invalidatedRectangle) {
if (!child._intersectsRect(invalidatedRectangle)) {
continue;
}
}
child._render(contextToDrawTo, invalidatedRectangle);
}
if (renderToIntermediateTextureThisDraw) {
contextToDrawTo.restore();
context.save();
context.globalAlpha = this.alpha;
context.drawImage(contextToDrawTo.canvas, this._currentMeasure.left, this._currentMeasure.top);
context.restore();
}
context.restore();
}
getDescendantsToRef(results, directDescendantsOnly = false, predicate) {
if (!this.children) {
return;
}
for (let index = 0; index < this.children.length; index++) {
const item = this.children[index];
if (!predicate || predicate(item)) {
results.push(item);
}
if (!directDescendantsOnly) {
item.getDescendantsToRef(results, false, predicate);
}
}
}
/**
* @internal
*/
_processPicking(x, y, pi, type, pointerId, buttonIndex, deltaX, deltaY) {
if (!this._isEnabled || !this.isVisible || this.notRenderable) {
return false;
}
// checks if the picking position is within the container
const contains = super.contains(x, y);
// if clipChildren is off, we should still pass picking events to children even if we don't contain the pointer
if (!contains && this.clipChildren) {
return false;
}
if (this.delegatePickingToChildren) {
let contains = false;
for (let index = this._children.length - 1; index >= 0; index--) {
const child = this._children[index];
if (child.isEnabled && child.isHitTestVisible && child.isVisible && !child.notRenderable && child.contains(x, y)) {
contains = true;
break;
}
}
if (!contains) {
return false;
}
}
// Checking backwards to pick closest first
for (let index = this._children.length - 1; index >= 0; index--) {
const child = this._children[index];
if (child._processPicking(x, y, pi, type, pointerId, buttonIndex, deltaX, deltaY)) {
if (child.hoverCursor) {
this._host._changeCursor(child.hoverCursor);
}
return true;
}
}
if (!contains) {
return false;
}
if (!this.isHitTestVisible) {
return false;
}
return this._processObservables(type, x, y, pi, pointerId, buttonIndex, deltaX, deltaY);
}
/**
* @internal
*/
_additionalProcessing(parentMeasure, context) {
super._additionalProcessing(parentMeasure, context);
this._measureForChildren.copyFrom(this._currentMeasure);
}
_getAdaptDimTo(dim) {
if (dim === "width") {
return this.adaptWidthToChildren;
}
else {
return this.adaptHeightToChildren;
}
}
isDimensionFullyDefined(dim) {
if (this._getAdaptDimTo(dim)) {
for (const child of this.children) {
if (!child.isDimensionFullyDefined(dim)) {
return false;
}
}
return true;
}
return super.isDimensionFullyDefined(dim);
}
/**
* Serializes the current control
* @param serializationObject defined the JSON serialized object
* @param force force serialization even if isSerializable === false
* @param allowCanvas defines if the control is allowed to use a Canvas2D object to serialize (true by default)
*/
serialize(serializationObject, force = false, allowCanvas = true) {
super.serialize(serializationObject, force, allowCanvas);
if (!this.isSerializable && !force) {
return;
}
if (this.backgroundGradient) {
serializationObject.backgroundGradient = {};
this.backgroundGradient.serialize(serializationObject.backgroundGradient);
}
if (!this.children.length) {
return;
}
serializationObject.children = [];
for (const child of this.children) {
if (child.isSerializable || force) {
const childSerializationObject = {};
child.serialize(childSerializationObject, force, allowCanvas);
serializationObject.children.push(childSerializationObject);
}
}
}
/** Releases associated resources */
dispose() {
super.dispose();
for (let index = this.children.length - 1; index >= 0; index--) {
this.children[index].dispose();
}
this._intermediateTexture?.dispose();
}
/**
* @internal
*/
_parseFromContent(serializedObject, host, urlRewriter) {
super._parseFromContent(serializedObject, host, urlRewriter);
this._link(host);
// Gradient
if (serializedObject.backgroundGradient) {
const className = Tools.Instantiate("BABYLON.GUI." + serializedObject.backgroundGradient.className);
this._backgroundGradient = new className();
this._backgroundGradient?.parse(serializedObject.backgroundGradient);
}
if (!serializedObject.children) {
return;
}
for (const childData of serializedObject.children) {
this.addControl(Control.Parse(childData, host, urlRewriter));
}
}
isReady() {
for (const child of this.children) {
if (!child.isReady()) {
return false;
}
}
return true;
}
}
__decorate([
serialize()
], Container.prototype, "delegatePickingToChildren", void 0);
__decorate([
serialize()
], Container.prototype, "renderToIntermediateTexture", null);
__decorate([
serialize()
], Container.prototype, "maxLayoutCycle", void 0);
__decorate([
serialize()
], Container.prototype, "adaptHeightToChildren", null);
__decorate([
serialize()
], Container.prototype, "adaptWidthToChildren", null);
__decorate([
serialize()
], Container.prototype, "background", null);
__decorate([
serialize()
], Container.prototype, "backgroundGradient", null);
RegisterClass("BABYLON.GUI.Container", Container);
//# sourceMappingURL=container.js.map