import { elt , removeChildrenAndAdd , removeChildren } from '../../../utils/dom' ; import EventEmitter from '../../../utils/emitter' ; export type FontOptions = { family ? : string ; size ? : number ; } ; /**
 * This class calculates font widths, heights and baselines. It also provides
 * some utility options like generating css style font strings (e.g., used when
 * setting the font of a canvas' context).
 */ export default class FontMetrics extends EventEmitter { private familyValue : string = "Monaco, 'Courier New', Courier, monospace" ; private sizeValue : number = 16 ; private measure : HTMLElement ; private cachedBaseline : number ; private cachedAsciiWidth : number ; private cachedLineHeight : number ; private cachedWidths : { [ char ] : number ; } = { } ; private ctx : CanvasRenderingContext2D ; param { HTMLElement } element param { FontOptions } opts constructor ( element , opts ) { super ( ) ; this . measure = element . appendChild ( elt ( 'div' , null , 'xi-measure' ) ) ; this . measure . style . position = 'absolute' ; this . measure . style . whiteSpace = 'nowrap' ; this . measure . style . visibility = 'hidden' ; const canvas = ( < HTMLCanvasElement > elt ( 'canvas' ) ) ; const ctx = canvas . getContext ( '2d' ) ; if ( ctx ) { this . ctx = ctx ; this . ctx . font = this . fontString ( ) ; } else { throw new Error ( 'Could not get CanvasRenderingContext2D' ) ; } this . update ( opts ) ; } /**
     * Public API
     */ param { FontOptions } opts public update ( opts ) { if ( opts . family ) { this . familyValue = opts . family ; } if ( opts . size ) { this . sizeValue = opts . size ; } const fontString = this . fontString ( ) ; this . ctx . font = this . measure . style . font = fontString ; this . cachedWidths = { } ; if ( this . cachedBaseline != null ) { this . baseline ( true ) ; } if ( this . cachedAsciiWidth != null ) { this . asciiWidth ( true ) ; } if ( this . cachedLineHeight != null ) { this . lineHeight ( true ) ; } this . emit ( 'update' ) ; } param { Boolean } force return { Number } public baseline ( force = false ) { if ( ! force && this . cachedBaseline != null ) { return this . cachedBaseline ; } return this . computeBaseline ( ) ; } param { String } char param { Boolean } force return { Number } public charWidth ( char , force = false ) { if ( ! force && this . cachedWidths [ char ] != null ) { return this . cachedWidths [ char ] ; } return this . computeCharWidth ( char ) ; } param { String } str return { Number } public stringWidth ( str ) { let width = 0 ; for ( const ch of str ) { width += this . charWidth ( ch ) ; } return width ; } param { Boolean } force return { Number } public asciiWidth ( force = false ) { if ( ! force && this . cachedAsciiWidth != null ) { return this . cachedAsciiWidth ; } return this . computeAsciiWidth ( ) ; } param { Boolean } force return { Number } public lineHeight ( force = false ) { if ( ! force && this . cachedLineHeight != null ) { return this . cachedLineHeight ; } return this . computeLineHeight ( ) ; } return { String } public fontString ( ) { return `${ this . sizeValue }px ${ this . familyValue }` ; } return { String } public family ( ) { return this . familyValue ; } return { Number } public size ( ) { return this . sizeValue ; } /**
     * Private API
     */ return { Number } private computeBaseline ( ) { const span = elt ( 'span' ) ; span . style . display = 'inline-block' ; span . style . overflow = 'hidden' ; span . style . width = '1px' ; span . style . height = '1px' ; removeChildrenAndAdd ( this . measure , span ) ; return this . cachedBaseline = span . offsetTop + span . offsetHeight ; } public computeCharWidth ( char ) { if ( this . cachedWidths [ char ] != null ) { return this . cachedWidths [ char ] ; } const width = this . ctx . measureText ( char ) . width ; this . cachedWidths [ char ] = width ; return width ; } return { Number } private computeAsciiWidth ( ) { const anchor = elt ( 'span' , 'x' . repeat ( 10 ) ) ; const pre = elt ( 'pre' , [ anchor ] ) ; removeChildrenAndAdd ( this . measure , pre ) ; const rect = anchor . getBoundingClientRect ( ) , width = ( rect . right - rect . left ) / 10 ; if ( width > 2 ) { this . cachedAsciiWidth = width ; } return width || 10 ; } return { Number } private computeLineHeight ( ) { const pre = elt ( 'pre' , null ) ; for ( let i = 0 ; i < 49 ; ++ i ) { pre . appendChild ( document . createTextNode ( 'x' ) ) ; pre . appendChild ( elt ( 'br' ) ) ; } pre . appendChild ( document . createTextNode ( 'x' ) ) ; removeChildrenAndAdd ( this . measure , pre ) ; const height = pre . offsetHeight / 50 ; if ( height > 3 ) { this . cachedLineHeight = height ; } return height || 1 ; } private computeTextHeight ( ) { const span = elt ( 'span' , 'Hg' , null , `font-family: ${ this . familyValue }` ) ; const div = elt ( 'div' , null , null , 'display: inline-block; width: 1px; height: 0' ) ; removeChildrenAndAdd ( this . measure , [ span , div ] ) ; const result = { ascent : 0 , descent : 0 , height : 0 } ; div . style . verticalAlign = 'baseline' ; result . ascent = div . getBoundingClientRect ( ) . top - span . getBoundingClientRect ( ) . top ; div . style . verticalAlign = 'bottom' ; result . height = div . getBoundingClientRect ( ) . top - span . getBoundingClientRect ( ) . top ; result . descent = result . height - result . ascent ; return result ; } }