import { Observable } from "babylonjs/Misc/observable" ; import { Measure } from "../measure" ; import { ValueAndUnit } from "../valueAndUnit" ; import { Control } from "./control" ; import { RegisterClass } from "babylonjs/Misc/typeStore" ; import { Nullable } from "babylonjs/types" ; import { serialize } from 'babylonjs/Misc/decorators' ; import { ICanvasRenderingContext } from 'babylonjs/Engines/ICanvas' ; import { Engine } from 'babylonjs/Engines/engine' ; /**
 * Enum that determines the text-wrapping mode to use.
 */ export enum TextWrapping { /**
     * Clip the text when it's larger than Control.width; this is the default mode.
     */ Clip = 0 , /**
     * Wrap the text word-wise, i.e. try to add line-breaks at word boundary to fit within Control.width.
     */ WordWrap = 1 , /**
     * Ellipsize the text, i.e. shrink with trailing … when text is larger than Control.width.
     */ Ellipsis = 2 , /**
     * 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 … .
     */ WordWrapEllipsis } /**
 * Class used to create text block control
 */ export class TextBlock extends Control { private _text = "" ; private _textWrapping = TextWrapping . Clip ; private _textHorizontalAlignment = Control . HORIZONTAL_ALIGNMENT_CENTER ; private _textVerticalAlignment = Control . VERTICAL_ALIGNMENT_CENTER ; private _lines : any [ ] ; private _resizeToFit : boolean = false ; private _lineSpacing : ValueAndUnit = new ValueAndUnit ( 0 ) ; private _outlineWidth : number = 0 ; private _outlineColor : string = "white" ; private _underline : boolean = false ; private _lineThrough : boolean = false ; /**
     * An event triggered after the text is changed
     */ public onTextChangedObservable = new Observable < TextBlock > ( ) ; /**
     * An event triggered after the text was broken up into lines
     */ public onLinesReadyObservable = new Observable < TextBlock > ( ) ; /**
     * Function used to split a string into words. By default, a string is split at each space character found
     */ public wordSplittingFunction : Nullable < ( line : string ) => string [ ] > ; /**
     * Return the line list (you may need to use the onLinesReadyObservable to make sure the list is ready)
     */ public get lines ( ) : any [ ] { return this . _lines ; } /**
     * Gets or sets an boolean indicating that the TextBlock will be resized to fit container
     */ @ serialize ( ) public get resizeToFit ( ) : boolean { return this . _resizeToFit ; } /**
     * Gets or sets an boolean indicating that the TextBlock will be resized to fit container
     */ public 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
     */ @ serialize ( ) public get textWrapping ( ) : TextWrapping | boolean { return this . _textWrapping ; } /**
     * Gets or sets a boolean indicating if text must be wrapped
     */ public set textWrapping ( value ) { if ( this . _textWrapping === value ) { return ; } this . _textWrapping = + value ; this . _markAsDirty ( ) ; } /**
     * Gets or sets text to display
     */ @ serialize ( ) public get text ( ) : string { return this . _text ; } /**
     * Gets or sets text to display
     */ public set text ( value ) { if ( this . _text === value ) { return ; } this . _text = value + "" ; this . _markAsDirty ( ) ; this . onTextChangedObservable . notifyObservers ( this ) ; } /**
     * Gets or sets text horizontal alignment (BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER by default)
     */ @ serialize ( ) public get textHorizontalAlignment ( ) : number { return this . _textHorizontalAlignment ; } /**
     * Gets or sets text horizontal alignment (BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER by default)
     */ public 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)
     */ @ serialize ( ) public get textVerticalAlignment ( ) : number { return this . _textVerticalAlignment ; } /**
     * Gets or sets text vertical alignment (BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER by default)
     */ public set textVerticalAlignment ( value ) { if ( this . _textVerticalAlignment === value ) { return ; } this . _textVerticalAlignment = value ; this . _markAsDirty ( ) ; } /**
     * Gets or sets line spacing value
     */ @ serialize ( ) public set lineSpacing ( value ) { if ( this . _lineSpacing . fromString ( value ) ) { this . _markAsDirty ( ) ; } } /**
     * Gets or sets line spacing value
     */ public get lineSpacing ( ) : string | number { return this . _lineSpacing . toString ( this . _host ) ; } /**
     * Gets or sets outlineWidth of the text to display
     */ @ serialize ( ) public get outlineWidth ( ) : number { return this . _outlineWidth ; } /**
     * Gets or sets outlineWidth of the text to display
     */ public set outlineWidth ( value ) { if ( this . _outlineWidth === value ) { return ; } this . _outlineWidth = value ; this . _markAsDirty ( ) ; } /**
     * Gets or sets a boolean indicating that text must have underline
     */ @ serialize ( ) public get underline ( ) : boolean { return this . _underline ; } /**
     * Gets or sets a boolean indicating that text must have underline
     */ public 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
     */ @ serialize ( ) public get lineThrough ( ) : boolean { return this . _lineThrough ; } /**
     * Gets or sets an boolean indicating that text must be crossed out
     */ public set lineThrough ( value ) { if ( this . _lineThrough === value ) { return ; } this . _lineThrough = value ; this . _markAsDirty ( ) ; } /**
     * Gets or sets outlineColor of the text to display
     */ @ serialize ( ) public get outlineColor ( ) : string { return this . _outlineColor ; } /**
     * Gets or sets outlineColor of the text to display
     */ public set outlineColor ( value ) { if ( this . _outlineColor === value ) { return ; } this . _outlineColor = value ; this . _markAsDirty ( ) ; } param name param text constructor ( /**
     * Defines the name of the control
     */ public name ? , text = "" ) { super ( name ) ; this . text = text ; } protected _getTypeName ( ) { return "TextBlock" ; } protected _processMeasures ( parentMeasure , context ) { if ( ! this . _fontOffset || this . isDirty ) { this . _fontOffset = Control . _GetFontOffset ( context . font ) ; } super . _processMeasures ( parentMeasure , context ) ; 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 === TextWrapping . Clip ) { let newWidth = ( this . _paddingLeftInPixels + this . _paddingRightInPixels + maxLineWidth ) | 0 ; if ( newWidth !== this . _width . internalValue ) { 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 ; } } } private _drawText ( text , textWidth , y , context ) { var width = this . _currentMeasure . width ; var 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 ) { context . beginPath ( ) ; context . lineWidth = Math . round ( this . fontSizeInPixels * 0.05 ) ; context . moveTo ( this . _currentMeasure . left + x , y + 3 ) ; context . lineTo ( this . _currentMeasure . left + x + textWidth , y + 3 ) ; context . stroke ( ) ; context . closePath ( ) ; } if ( this . _lineThrough ) { context . beginPath ( ) ; context . lineWidth = Math . round ( this . fontSizeInPixels * 0.05 ) ; context . moveTo ( this . _currentMeasure . left + x , y - this . fontSizeInPixels / 3 ) ; context . lineTo ( this . _currentMeasure . left + x + textWidth , y - this . fontSizeInPixels / 3 ) ; context . stroke ( ) ; context . closePath ( ) ; } } hidden public _draw ( context , invalidatedRectangle ? ) { context . save ( ) ; this . _applyStates ( context ) ; this . _renderLines ( context ) ; context . restore ( ) ; } protected _applyStates ( context ) { super . _applyStates ( context ) ; if ( this . outlineWidth ) { context . lineWidth = this . outlineWidth ; context . strokeStyle = this . outlineColor ; context . lineJoin = 'miter' ; context . miterLimit = 2 ; } } protected _breakLines ( refWidth , refHeight , context ) { var lines = [ ] ; var _lines = this . text . split ( "\n" ) ; if ( this . _textWrapping === TextWrapping . Ellipsis ) { for ( var _line of _lines ) { lines . push ( this . _parseLineEllipsis ( _line , refWidth , context ) ) ; } } else if ( this . _textWrapping === TextWrapping . WordWrap ) { for ( var _line of _lines ) { lines . push ( ... this . _parseLineWordWrap ( _line , refWidth , context ) ) ; } } else if ( this . _textWrapping === TextWrapping . WordWrapEllipsis ) { for ( var _line of _lines ) { lines . push ( ... this . _parseLineWordWrapEllipsis ( _line , refWidth , refHeight ! , context ) ) ; } } else { for ( var _line of _lines ) { lines . push ( this . _parseLine ( _line , context ) ) ; } } return lines ; } protected _parseLine ( line = "" , context ) { var textMetrics = context . measureText ( line ) ; var lineWidth = Math . abs ( textMetrics . actualBoundingBoxLeft ) + Math . abs ( textMetrics . actualBoundingBoxRight ) ; return { text : line , width : lineWidth } ; } protected _parseLineEllipsis ( line = "" , width , context ) { var textMetrics = context . measureText ( line ) ; var lineWidth = Math . abs ( textMetrics . actualBoundingBoxLeft ) + Math . abs ( textMetrics . actualBoundingBoxRight ) ; if ( lineWidth > width ) { line += "…" ; } const characters = Array . from && Array . from ( line ) ; if ( ! characters ) { while ( line . length > 2 && lineWidth > width ) { line = line . slice ( 0 , - 2 ) + "…" ; textMetrics = context . measureText ( line ) ; lineWidth = Math . abs ( textMetrics . actualBoundingBoxLeft ) + Math . abs ( textMetrics . actualBoundingBoxRight ) ; } } else { while ( characters . length && lineWidth > width ) { characters . pop ( ) ; line = `${ characters . join ( "" ) }...` ; textMetrics = context . measureText ( line ) ; lineWidth = Math . abs ( textMetrics . actualBoundingBoxLeft ) + Math . abs ( textMetrics . actualBoundingBoxRight ) ; } } return { text : line , width : lineWidth } ; } protected _parseLineWordWrap ( line = "" , width , context ) { var lines = [ ] ; var words = this . wordSplittingFunction ? this . wordSplittingFunction ( line ) : line . split ( " " ) ; var textMetrics = context . measureText ( line ) ; var lineWidth = Math . abs ( textMetrics . actualBoundingBoxLeft ) + Math . abs ( textMetrics . actualBoundingBoxRight ) ; for ( var n = 0 ; n < words . length ; n ++ ) { var testLine = n > 0 ? line + " " + words [ n ] : words [ 0 ] ; var metrics = context . measureText ( testLine ) ; var testWidth = Math . abs ( metrics . actualBoundingBoxLeft ) + Math . abs ( metrics . actualBoundingBoxRight ) ; if ( testWidth > width && n > 0 ) { lines . push ( { text : line , width : lineWidth } ) ; line = words [ n ] ; textMetrics = context . measureText ( line ) ; lineWidth = Math . abs ( textMetrics . actualBoundingBoxLeft ) + Math . abs ( textMetrics . actualBoundingBoxRight ) ; } else { lineWidth = testWidth ; line = testLine ; } } lines . push ( { text : line , width : lineWidth } ) ; return lines ; } protected _parseLineWordWrapEllipsis ( line = "" , width , height , context ) { var lines = this . _parseLineWordWrap ( line , width , context ) ; for ( var n = 1 ; n <= lines . length ; n ++ ) { const currentHeight = this . _computeHeightForLinesOf ( n ) ; if ( currentHeight > height && n > 1 ) { var lastLine = lines [ n - 2 ] as { text : string ; width : number ; } ; var currentLine = lines [ n - 1 ] as { text : string ; width : number ; } ; lines [ n - 2 ] = this . _parseLineEllipsis ( `${ lastLine . text + currentLine . text }` , width , context ) ; var linesToRemove = lines . length - n + 1 ; for ( var i = 0 ; i < linesToRemove ; i ++ ) { lines . pop ( ) ; } return lines ; } } return lines ; } protected _renderLines ( context ) { var height = this . _currentMeasure . height ; var 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 ; } } private _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 ; } returns public computeExpectedHeight ( ) { if ( this . text && this . widthInPixels ) { const context = Engine . LastCreatedEngine ?. createCanvas ( 0 , 0 ) . getContext ( "2d" ) ; if ( context ) { this . _applyStates ( context ) ; if ( ! this . _fontOffset ) { this . _fontOffset = Control . _GetFontOffset ( context . font ) ; } 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 ( ) ; } } RegisterClass ( "BABYLON.GUI.TextBlock" , TextBlock ) ;