UNPKG

@babylonjs/gui

Version:

Babylon.js GUI module =====================

673 lines (671 loc) 24.9 kB
import { __decorate } from "@babylonjs/core/tslib.es6.js"; import { Observable } from "@babylonjs/core/Misc/observable.js"; import { ValueAndUnit } from "../valueAndUnit.js"; import { Control } from "./control.js"; import { RegisterClass } from "@babylonjs/core/Misc/typeStore.js"; import { serialize } from "@babylonjs/core/Misc/decorators.js"; import { EngineStore } from "@babylonjs/core/Engines/engineStore.js"; /** * Enum that determines the text-wrapping mode to use. */ export var TextWrapping; (function (TextWrapping) { /** * Clip the text when it's larger than Control.width; this is the default mode. */ TextWrapping[TextWrapping["Clip"] = 0] = "Clip"; /** * Wrap the text word-wise, i.e. try to add line-breaks at word boundary to fit within Control.width. */ TextWrapping[TextWrapping["WordWrap"] = 1] = "WordWrap"; /** * Ellipsize the text, i.e. shrink with trailing … when text is larger than Control.width. */ TextWrapping[TextWrapping["Ellipsis"] = 2] = "Ellipsis"; /** * Wrap the text word-wise and clip the text when the text's height is larger than the Control.height, and shrink the last line with trailing … . */ TextWrapping[TextWrapping["WordWrapEllipsis"] = 3] = "WordWrapEllipsis"; /** * Use HTML to wrap the text. This is the only mode that supports east-asian languages. */ TextWrapping[TextWrapping["HTML"] = 4] = "HTML"; })(TextWrapping || (TextWrapping = {})); /** * Class used to create text block control */ export class TextBlock extends Control { /** * Return the line list (you may need to use the onLinesReadyObservable to make sure the list is ready) */ get lines() { return this._lines; } /** * Gets or sets a boolean indicating that the TextBlock will be resized to fit its content */ get resizeToFit() { return this._resizeToFit; } /** * Gets or sets a boolean indicating that the TextBlock will be resized to fit its content */ set resizeToFit(value) { if (this._resizeToFit === value) { return; } this._resizeToFit = value; if (this._resizeToFit) { this._width.ignoreAdaptiveScaling = true; this._height.ignoreAdaptiveScaling = true; } this._markAsDirty(); } /** * Gets or sets a boolean indicating if text must be wrapped */ get textWrapping() { return this._textWrapping; } /** * Gets or sets a boolean indicating if text must be wrapped */ set textWrapping(value) { if (this._textWrapping === value) { return; } this._textWrapping = +value; this._markAsDirty(); } /** * Gets or sets text to display */ get text() { return this._text; } /** * Gets or sets text to display */ set text(value) { if (this._text === value) { return; } this._text = value + ""; // Making sure it is a text this._markAsDirty(); this.onTextChangedObservable.notifyObservers(this); } /** * Gets or sets text horizontal alignment (BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER by default) */ get textHorizontalAlignment() { return this._textHorizontalAlignment; } /** * Gets or sets text horizontal alignment (BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER by default) */ set textHorizontalAlignment(value) { if (this._textHorizontalAlignment === value) { return; } this._textHorizontalAlignment = value; this._markAsDirty(); } /** * Gets or sets text vertical alignment (BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER by default) */ get textVerticalAlignment() { return this._textVerticalAlignment; } /** * Gets or sets text vertical alignment (BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER by default) */ set textVerticalAlignment(value) { if (this._textVerticalAlignment === value) { return; } this._textVerticalAlignment = value; this._markAsDirty(); } /** * Gets or sets line spacing value */ set lineSpacing(value) { if (this._lineSpacing.fromString(value)) { this._markAsDirty(); } } /** * Gets or sets line spacing value */ get lineSpacing() { return this._lineSpacing.toString(this._host); } /** * Gets or sets outlineWidth of the text to display */ get outlineWidth() { return this._outlineWidth; } /** * Gets or sets outlineWidth of the text to display */ set outlineWidth(value) { if (this._outlineWidth === value) { return; } this._outlineWidth = value; this._markAsDirty(); } /** * Gets or sets a boolean indicating that text must have underline */ get underline() { return this._underline; } /** * Gets or sets a boolean indicating that text must have underline */ set underline(value) { if (this._underline === value) { return; } this._underline = value; this._markAsDirty(); } /** * Gets or sets an boolean indicating that text must be crossed out */ get lineThrough() { return this._lineThrough; } /** * Gets or sets an boolean indicating that text must be crossed out */ set lineThrough(value) { if (this._lineThrough === value) { return; } this._lineThrough = value; this._markAsDirty(); } /** * If the outline should be applied to the underline/strike-through too. Has different behavior in Edge/Chrome vs Firefox. */ get applyOutlineToUnderline() { return this._applyOutlineToUnderline; } set applyOutlineToUnderline(value) { if (this._applyOutlineToUnderline === value) { return; } this._applyOutlineToUnderline = value; this._markAsDirty(); } /** * Gets or sets outlineColor of the text to display */ get outlineColor() { return this._outlineColor; } /** * Gets or sets outlineColor of the text to display */ set outlineColor(value) { if (this._outlineColor === value) { return; } this._outlineColor = value; this._markAsDirty(); } /** * Gets or sets word divider */ get wordDivider() { return this._wordDivider; } /** * Gets or sets word divider */ set wordDivider(value) { if (this._wordDivider === value) { return; } this._wordDivider = value; this._markAsDirty(); } /** * By default, if a text block has text wrapping other than Clip, its width * is not resized even if resizeToFit = true. This parameter forces the width * to be resized. */ get forceResizeWidth() { return this._forceResizeWidth; } set forceResizeWidth(value) { if (this._forceResizeWidth === value) { return; } this._forceResizeWidth = value; this._markAsDirty(); } /** * Creates a new TextBlock object * @param name defines the name of the control * @param text defines the text to display (empty string by default) */ constructor( /** * Defines the name of the control */ name, text = "") { super(name); this.name = name; this._text = ""; this._textWrapping = 0 /* TextWrapping.Clip */; this._textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; this._textVerticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; this._resizeToFit = false; this._lineSpacing = new ValueAndUnit(0); this._outlineWidth = 0; this._outlineColor = "white"; this._underline = false; this._lineThrough = false; this._wordDivider = " "; this._forceResizeWidth = false; this._applyOutlineToUnderline = false; /** * An event triggered after the text is changed */ this.onTextChangedObservable = new Observable(); /** * An event triggered after the text was broken up into lines */ this.onLinesReadyObservable = new Observable(); /** * Gets or sets a boolean indicating if the HTML element generated for word wrapping should be reused or removed after each wrapping. */ this.reuseHTMLForWordWrapping = false; this._linesTemp = []; this._htmlElement = null; this.text = text; } _getTypeName() { return "TextBlock"; } _processMeasures(parentMeasure, context) { // Ensure this is done first so that applyStates is called before remainder of work. If it turns out // we need fontOffset to be set before specific logic inside processMeasures, we can move the below // fontOffset check inside super.processMeasures super._processMeasures(parentMeasure, context); if (!this._fontOffset || this.isDirty) { this._fontOffset = Control._GetFontOffset(context.font, this._host.getScene()?.getEngine()); } // Prepare lines this._lines = this._breakLines(this._currentMeasure.width, this._currentMeasure.height, context); this.onLinesReadyObservable.notifyObservers(this); let maxLineWidth = 0; for (let i = 0; i < this._lines.length; i++) { const line = this._lines[i]; if (line.width > maxLineWidth) { maxLineWidth = line.width; } } if (this._resizeToFit) { if (this._textWrapping === 0 /* TextWrapping.Clip */ || this._forceResizeWidth) { const newWidth = Math.ceil(this._paddingLeftInPixels) + Math.ceil(this._paddingRightInPixels) + Math.ceil(maxLineWidth); if (newWidth !== this._width.getValueInPixel(this._host, this._tempParentMeasure.width)) { this._width.updateInPlace(newWidth, ValueAndUnit.UNITMODE_PIXEL); this._rebuildLayout = true; } } let newHeight = (this._paddingTopInPixels + this._paddingBottomInPixels + this._fontOffset.height * this._lines.length) | 0; if (this._lines.length > 0 && this._lineSpacing.internalValue !== 0) { let lineSpacing = 0; if (this._lineSpacing.isPixel) { lineSpacing = this._lineSpacing.getValue(this._host); } else { lineSpacing = this._lineSpacing.getValue(this._host) * this._height.getValueInPixel(this._host, this._cachedParentMeasure.height); } newHeight += (this._lines.length - 1) * lineSpacing; } if (newHeight !== this._height.internalValue) { this._height.updateInPlace(newHeight, ValueAndUnit.UNITMODE_PIXEL); this._rebuildLayout = true; } } } _drawText(text, textWidth, y, context) { const width = this._currentMeasure.width; let x = 0; switch (this._textHorizontalAlignment) { case Control.HORIZONTAL_ALIGNMENT_LEFT: x = 0; break; case Control.HORIZONTAL_ALIGNMENT_RIGHT: x = width - textWidth; break; case Control.HORIZONTAL_ALIGNMENT_CENTER: x = (width - textWidth) / 2; break; } if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) { context.shadowColor = this.shadowColor; context.shadowBlur = this.shadowBlur; context.shadowOffsetX = this.shadowOffsetX; context.shadowOffsetY = this.shadowOffsetY; } if (this.outlineWidth) { context.strokeText(text, this._currentMeasure.left + x, y); } context.fillText(text, this._currentMeasure.left + x, y); if (this._underline) { this._drawLine(this._currentMeasure.left + x, y + 3, this._currentMeasure.left + x + textWidth, y + 3, context); } if (this._lineThrough) { this._drawLine(this._currentMeasure.left + x, y - this.fontSizeInPixels / 3, this._currentMeasure.left + x + textWidth, y - this.fontSizeInPixels / 3, context); } } _drawLine(xFrom, yFrom, xTo, yTo, context) { context.beginPath(); context.lineWidth = Math.round(this.fontSizeInPixels * 0.05); context.moveTo(xFrom, yFrom); context.lineTo(xTo, yTo); if (this.outlineWidth && this.applyOutlineToUnderline) { context.stroke(); context.fill(); } else { const currentStroke = context.strokeStyle; context.strokeStyle = context.fillStyle; context.stroke(); context.strokeStyle = currentStroke; } context.closePath(); } /** * @internal */ _draw(context) { context.save(); this._applyStates(context); // Render lines this._renderLines(context); context.restore(); } _applyStates(context) { super._applyStates(context); if (this.outlineWidth) { context.lineWidth = this.outlineWidth; context.strokeStyle = this.outlineColor; context.lineJoin = "miter"; context.miterLimit = 2; } } _breakLines(refWidth, refHeight, context) { this._linesTemp.length = 0; const _lines = this._textWrapping === 4 /* TextWrapping.HTML */ ? this._parseHTMLText(refWidth, refHeight, context) : this.text.split("\n"); switch (this._textWrapping) { case 1 /* TextWrapping.WordWrap */: for (const _line of _lines) { this._linesTemp.push(...this._parseLineWordWrap(_line, refWidth, context)); } break; case 2 /* TextWrapping.Ellipsis */: for (const _line of _lines) { this._linesTemp.push(this._parseLineEllipsis(_line, refWidth, context)); } break; case 3 /* TextWrapping.WordWrapEllipsis */: for (const _line of _lines) { this._linesTemp.push(...this._parseLineWordWrapEllipsis(_line, refWidth, refHeight, context)); } break; case 4 /* TextWrapping.HTML */: default: for (const _line of _lines) { this._linesTemp.push(this._parseLine(_line, context)); } break; } return this._linesTemp; } _parseHTMLText(refWidth, refHeight, context) { const lines = []; if (!this._htmlElement) { this._htmlElement = document.createElement("div"); document.body.appendChild(this._htmlElement); } const htmlElement = this._htmlElement; htmlElement.textContent = this.text; htmlElement.style.font = context.font; htmlElement.style.position = "absolute"; htmlElement.style.visibility = "hidden"; htmlElement.style.top = "-1000px"; htmlElement.style.left = "-1000px"; this.adjustWordWrappingHTMLElement?.(htmlElement); htmlElement.style.width = refWidth + "px"; htmlElement.style.height = refHeight + "px"; const textContent = htmlElement.textContent; if (!textContent) { return lines; } // get the text node const textNode = htmlElement.childNodes[0]; const range = document.createRange(); let idx = 0; for (const c of textContent) { range.setStart(textNode, 0); range.setEnd(textNode, idx + 1); // "select" text from beginning to this position to determine the line const lineIndex = range.getClientRects().length - 1; lines[lineIndex] = (lines[lineIndex] || "") + c; idx++; } if (!this.reuseHTMLForWordWrapping) { htmlElement.remove(); this._htmlElement = null; } return lines; } _parseLine(line = "", context) { return { text: line, width: this._getTextMetricsWidth(context.measureText(line)) }; } //Calculate how many characters approximately we need to remove _getCharsToRemove(lineWidth, width, lineLength) { const diff = lineWidth > width ? lineWidth - width : 0; // This isn't exact unless the font is monospaced const charWidth = lineWidth / lineLength; const removeChars = Math.max(Math.floor(diff / charWidth), 1); return removeChars; } _parseLineEllipsis(line = "", width, context) { let lineWidth = this._getTextMetricsWidth(context.measureText(line)); let removeChars = this._getCharsToRemove(lineWidth, width, line.length); // unicode support. split('') does not work with unicode! // make sure Array.from is available const characters = Array.from && Array.from(line); if (!characters) { // no array.from, use the old method while (line.length > 2 && lineWidth > width) { line = line.slice(0, -removeChars); lineWidth = this._getTextMetricsWidth(context.measureText(line + "…")); removeChars = this._getCharsToRemove(lineWidth, width, line.length); } // Add on the end line += "…"; } else { while (characters.length && lineWidth > width) { characters.splice(characters.length - removeChars, removeChars); line = `${characters.join("")}…`; lineWidth = this._getTextMetricsWidth(context.measureText(line)); removeChars = this._getCharsToRemove(lineWidth, width, line.length); } } return { text: line, width: lineWidth }; } _getTextMetricsWidth(textMetrics) { if (textMetrics.actualBoundingBoxLeft !== undefined) { return Math.abs(textMetrics.actualBoundingBoxLeft) + Math.abs(textMetrics.actualBoundingBoxRight); } return textMetrics.width; } _parseLineWordWrap(line = "", width, context) { const lines = []; const words = this.wordSplittingFunction ? this.wordSplittingFunction(line) : line.split(this._wordDivider); let lineWidth = this._getTextMetricsWidth(context.measureText(line)); for (let n = 0; n < words.length; n++) { const testLine = n > 0 ? line + this._wordDivider + words[n] : words[0]; const testWidth = this._getTextMetricsWidth(context.measureText(testLine)); if (testWidth > width && n > 0) { lines.push({ text: line, width: lineWidth }); line = words[n]; lineWidth = this._getTextMetricsWidth(context.measureText(line)); } else { lineWidth = testWidth; line = testLine; } } lines.push({ text: line, width: lineWidth }); return lines; } _parseLineWordWrapEllipsis(line = "", width, height, context) { const lines = this._parseLineWordWrap(line, width, context); for (let n = 1; n <= lines.length; n++) { const currentHeight = this._computeHeightForLinesOf(n); if (currentHeight > height && n > 1) { const lastLine = lines[n - 2]; const currentLine = lines[n - 1]; lines[n - 2] = this._parseLineEllipsis(lastLine.text + this._wordDivider + currentLine.text, width, context); const linesToRemove = lines.length - n + 1; for (let i = 0; i < linesToRemove; i++) { lines.pop(); } return lines; } } return lines; } _renderLines(context) { if (!this._fontOffset || !this._lines) { return; } const height = this._currentMeasure.height; let rootY = 0; switch (this._textVerticalAlignment) { case Control.VERTICAL_ALIGNMENT_TOP: rootY = this._fontOffset.ascent; break; case Control.VERTICAL_ALIGNMENT_BOTTOM: rootY = height - this._fontOffset.height * (this._lines.length - 1) - this._fontOffset.descent; break; case Control.VERTICAL_ALIGNMENT_CENTER: rootY = this._fontOffset.ascent + (height - this._fontOffset.height * this._lines.length) / 2; break; } rootY += this._currentMeasure.top; for (let i = 0; i < this._lines.length; i++) { const line = this._lines[i]; if (i !== 0 && this._lineSpacing.internalValue !== 0) { if (this._lineSpacing.isPixel) { rootY += this._lineSpacing.getValue(this._host); } else { rootY = rootY + this._lineSpacing.getValue(this._host) * this._height.getValueInPixel(this._host, this._cachedParentMeasure.height); } } this._drawText(line.text, line.width, rootY, context); rootY += this._fontOffset.height; } } _computeHeightForLinesOf(lineCount) { let newHeight = this._paddingTopInPixels + this._paddingBottomInPixels + this._fontOffset.height * lineCount; if (lineCount > 0 && this._lineSpacing.internalValue !== 0) { let lineSpacing = 0; if (this._lineSpacing.isPixel) { lineSpacing = this._lineSpacing.getValue(this._host); } else { lineSpacing = this._lineSpacing.getValue(this._host) * this._height.getValueInPixel(this._host, this._cachedParentMeasure.height); } newHeight += (lineCount - 1) * lineSpacing; } return newHeight; } isDimensionFullyDefined(dim) { if (this.resizeToFit) { return true; } return super.isDimensionFullyDefined(dim); } /** * Given a width constraint applied on the text block, find the expected height * @returns expected height */ computeExpectedHeight() { if (this.text && this.widthInPixels) { // Should abstract platform instead of using LastCreatedEngine const context = EngineStore.LastCreatedEngine?.createCanvas(0, 0).getContext("2d"); if (context) { this._applyStates(context); if (!this._fontOffset) { this._fontOffset = Control._GetFontOffset(context.font, this._host.getScene()?.getEngine()); } const lines = this._lines ? this._lines : this._breakLines(this.widthInPixels - this._paddingLeftInPixels - this._paddingRightInPixels, this.heightInPixels - this._paddingTopInPixels - this._paddingBottomInPixels, context); return this._computeHeightForLinesOf(lines.length); } } return 0; } dispose() { super.dispose(); this.onTextChangedObservable.clear(); this._htmlElement?.remove(); this._htmlElement = null; } } __decorate([ serialize() ], TextBlock.prototype, "resizeToFit", null); __decorate([ serialize() ], TextBlock.prototype, "textWrapping", null); __decorate([ serialize() ], TextBlock.prototype, "text", null); __decorate([ serialize() ], TextBlock.prototype, "textHorizontalAlignment", null); __decorate([ serialize() ], TextBlock.prototype, "textVerticalAlignment", null); __decorate([ serialize() ], TextBlock.prototype, "lineSpacing", null); __decorate([ serialize() ], TextBlock.prototype, "outlineWidth", null); __decorate([ serialize() ], TextBlock.prototype, "underline", null); __decorate([ serialize() ], TextBlock.prototype, "lineThrough", null); __decorate([ serialize() ], TextBlock.prototype, "applyOutlineToUnderline", null); __decorate([ serialize() ], TextBlock.prototype, "outlineColor", null); __decorate([ serialize() ], TextBlock.prototype, "wordDivider", null); __decorate([ serialize() ], TextBlock.prototype, "forceResizeWidth", null); RegisterClass("BABYLON.GUI.TextBlock", TextBlock); //# sourceMappingURL=textBlock.js.map