import { elt , on } from '../../../utils/dom' ; import { clamp , nDigits } from '../../../utils/misc' ; import { COLORS , StyleSpan } from '../../style-map' ; import { View , ViewOptions , Viewport } from '../index' ; import ViewController from '../../view-controller' ; import LineCache from '../../line-cache' ; import FontMetrics from './font-metrics' ; export default class CanvasView implements View { private wrapper : HTMLElement ; private canvas : HTMLCanvasElement ; private ctx : CanvasRenderingContext2D ; private metrics : FontMetrics ; private lineCache : LineCache ; private scale : number = ( window . devicePixelRatio || 1 ) ; private x : number = 0 ; private y : number = 0 ; private width : number = 0 ; private height : number = 0 ; private gutterChars : number = 0 ; private gutterWidth : number = 0 ; private editorPadding : [ number , number ] = [ 5 , 0 ] ; private gutterPadding : [ number , number ] = [ 20 , 0 ] ; private scrollPastEnd : boolean = false ; private drawGutter : boolean = true ; private nChars : number = 0 ; constructor ( private controller , opts ) { this . wrapper = controller . wrapper ; this . lineCache = controller . lineCache ; this . metrics = new FontMetrics ( this . wrapper , { family : 'monospace' , size : 20 } ) ; [ 'drawGutter' , 'gutterPadding' , 'editorPadding' , 'scrollPastEnd' ] . forEach ( ( key ) => { if ( opts [ key ] ) { this [ key ] = opts [ key ] ; } } ) ; this . canvas = ( < HTMLCanvasElement > this . wrapper . appendChild ( elt ( 'canvas' ) ) ) ; this . canvas . style . display = 'block' ; on ( this . canvas , 'mousewheel' , ( event ) => this . scrollCanvas ( event ) , false ) ; const ctx = this . canvas . getContext ( '2d' ) ; if ( ctx ) { this . ctx = ctx ; } else { throw new Error ( 'Could not get CanvasRenderingContext2D' ) ; } this . metrics . on ( 'update' , ( ) => { this . measureGutter ( ) ; console . warn ( 'unimplemented' ) ; } ) ; this . lineCache . on ( 'update' , ( ) => this . measureGutter ( ) ) ; } private measureGutter ( ) { if ( this . drawGutter ) { const asciiWidth = this . metrics . asciiWidth ( ) ; this . gutterChars = nDigits ( this . lineCache . height ( ) ) ; this . gutterWidth = this . gutterChars * asciiWidth + this . gutterPadding [ 0 ] ; } else { this . gutterWidth = 0 ; } } param event private scrollCanvas ( event ) { let { deltaX , deltaY } = event ; const { charStart , charEnd , lineStart , lineEnd } = this . getViewport ( ) ; const nChars = this . nChars + Math . ceil ( charStart - charEnd ) + 5 ; let nLines ; if ( this . scrollPastEnd ) { nLines = this . lineCache . height ( ) ; } else { nLines = this . lineCache . height ( ) + lineStart - lineEnd ; } const asciiWidth = this . metrics . asciiWidth ( ) ; const lineHeight = this . metrics . lineHeight ( ) ; this . x = clamp ( this . x + deltaX , 0 , Math . max ( nChars * asciiWidth , 0 ) ) ; this . y = clamp ( this . y + deltaY , 0 , Math . max ( ( nLines - 1 ) * lineHeight , 0 ) ) ; this . controller . updateViewport ( ) ; this . render ( ) ; } /**
     * Public API
     */ /**
     * Called whenever the view is resized.
     */ public resize ( width , height ) { this . canvas . width = width * this . scale ; this . canvas . height = height * this . scale ; this . canvas . style . width = `${ width }px` ; this . canvas . style . height = `${ height }px` ; this . ctx . scale ( this . scale , this . scale ) ; this . width = width ; this . height = height ; this . render ( ) ; } param { Number } x param { Number } y return { Array } public posFromCoords ( x , y ) { const lineHeight = this . metrics . lineHeight ( ) ; const lineNo = Math . round ( ( ( y + this . y ) - ( lineHeight / 2 ) ) / lineHeight ) ; const line = this . lineCache . get ( lineNo ) ; if ( ! line ) { throw new Error ( `Failed to find line at [x, y]: [${ x }, ${ y }]` ) ; } x += this . x - this . gutterWidth ; let left = 0 , pos = 0 ; for ( const ch of line . text ) { const charWidth = this . metrics . charWidth ( ch ) ; if ( left < x && x < ( left + charWidth ) ) { return [ lineNo , line . chTo8Indices [ pos ] ] ; } left += charWidth ; pos += 1 ; } return [ lineNo , line . chTo8Indices [ line . chTo8Indices . length - 1 ] ] ; } param { number } line param { number } char public scrollTo ( line , char ) { const asciiWidth = this . metrics . asciiWidth ( ) ; const lineHeight = this . metrics . lineHeight ( ) ; const linePos = lineHeight * line ; const charPos = asciiWidth * char ; if ( linePos < this . y ) { this . y = linePos ; } else if ( linePos > this . y + this . height - lineHeight ) { this . y = linePos - this . height + lineHeight ; } if ( charPos < this . x ) { this . x = charPos ; } else if ( charPos > this . x + this . width - this . gutterWidth - asciiWidth ) { this . x = charPos + this . gutterWidth + asciiWidth - this . width ; } this . render ( ) ; } return { Object } public getViewport ( ) { const asciiWidth = this . metrics . asciiWidth ( ) ; const lineHeight = this . metrics . lineHeight ( ) ; const xOffset = this . gutterWidth - this . editorPadding [ 0 ] ; return { lineStart : Math . floor ( this . y / lineHeight ) , lineEnd : Math . floor ( ( this . height + this . y ) / lineHeight ) , charStart : Math . floor ( this . x / asciiWidth ) , charEnd : Math . floor ( ( this . width + this . x - xOffset ) / asciiWidth ) , } ; } /**
     * Renders the document onto the canvas.
     */ public render ( ) { const baseline = this . metrics . baseline ( ) ; const lineHeight = this . metrics . lineHeight ( ) ; const { editorPadding , gutterPadding , gutterWidth } = this ; const xOffset = gutterWidth + editorPadding [ 0 ] - this . x ; const xViewportEnd = this . width + this . x - xOffset ; this . ctx . font = this . metrics . fontString ( ) ; this . ctx . fillStyle = COLORS . BACKGROUND ; this . ctx . fillRect ( 0 , 0 , this . canvas . width , this . canvas . height ) ; const { lineStart , lineEnd } = this . getViewport ( ) ; this . lineCache . computeMissing ( lineStart , lineEnd ) ; const getLineData = ( i ) => ( { y : ( lineHeight * i ) - this . y , line : this . lineCache . get ( i ) } ) ; for ( let i = lineStart ; i <= lineEnd ; ++ i ) { const { line , y } = getLineData ( i ) ; if ( ! line || ! line . containsReservedStyle ( ) ) { continue ; } const renderBlock = ( styleSpans ) => { if ( styleSpans . length ) { this . ctx . fillStyle = styleSpans [ 0 ] . style . bg ; styleSpans . forEach ( ( { range : { start , length } } ) => { const a = line . chTo16Indices [ start ] ; const b = line . chTo16Indices [ start + length ] ; const beforeTextWidth = this . metrics . stringWidth ( line . text . substring ( 0 , a ) ) ; const textWidth = this . metrics . stringWidth ( line . text . substring ( a , b ) ) ; this . ctx . fillRect ( beforeTextWidth + xOffset , y , textWidth , lineHeight ) ; } ) ; } } ; renderBlock ( line . styles . filter ( ( span ) => span . style . isSelection ( ) ) ) ; renderBlock ( line . styles . filter ( ( span ) => span . style . isHighlight ( ) ) ) ; } this . ctx . save ( ) ; for ( let i = lineStart ; i <= lineEnd ; ++ i ) { const { line , y } = getLineData ( i ) ; if ( ! line ) { continue ; } this . nChars = Math . max ( this . nChars , line . text . length ) ; this . ctx . fillStyle = COLORS . CURSOR ; line . cursors . forEach ( ( ch ) => { const textWidth = this . metrics . stringWidth ( line . text . substring ( 0 , line . chTo16Indices [ ch ] ) ) ; this . ctx . fillRect ( textWidth + xOffset , y , 2 , lineHeight ) ; } ) ; let posX = 0 ; const textY = y + baseline ; for ( let i = 0 ; i < line . styles . length ; ++ i ) { const { style , range : { start , length } } = line . styles [ i ] ; if ( posX > xViewportEnd ) { break ; } if ( style . isReservedStyle ( ) ) { continue ; } let a = start ; let b = start + length ; const span = line . text . substring ( line . chTo16Indices [ a ] , line . chTo16Indices [ b ] ) ; let foundStart = false ; let pos = 0 ; let textX = 0 ; for ( const ch of span ) { const charWidth = this . metrics . charWidth ( ch ) ; if ( ! foundStart ) { if ( posX + charWidth > this . x ) { foundStart = true ; textX = posX + xOffset ; } else { a += 1 ; } } if ( posX > xViewportEnd ) { b -= length - pos ; break ; } posX += charWidth ; pos += 1 ; } if ( posX < this . x ) { continue ; } const text = line . text . substring ( line . chTo16Indices [ a ] , line . chTo16Indices [ b ] ) ; if ( text . length > 0 ) { this . ctx . fillStyle = style . fg ; this . ctx . font = style . fontString ( this . metrics ) ; this . ctx . fillText ( text , textX , textY ) ; } } } this . ctx . restore ( ) ; if ( this . drawGutter ) { this . ctx . fillStyle = '#242424' ; this . ctx . fillRect ( 0 , 0 , gutterWidth , this . height ) ; this . ctx . strokeStyle = '#5a5a5a' ; this . ctx . beginPath ( ) ; this . ctx . moveTo ( gutterWidth , 0 ) ; this . ctx . lineTo ( gutterWidth , this . height ) ; this . ctx . stroke ( ) ; this . ctx . fillStyle = '#5a5a5a' ; for ( let i = lineStart ; i <= lineEnd ; ++ i ) { const { line , y } = getLineData ( i ) ; if ( ! line ) { continue ; } const text = `${ i + 1 }` . padStart ( this . gutterChars , ' ' ) ; this . ctx . fillText ( text , gutterPadding [ 0 ] / 2 , y + baseline ) ; } } } } /**
 * TODO:
 *  - implement blinking cursors
 *  - find a better way to get the longest line in editor
 *  - invalidate parts of the canvas, to decrease load
 *  - introduce custom dpi scaling? (https://github.com/niklasvh/html2canvas/pull/1087/files)
 *  - when `metrics` are updated, get current scrollTo pos then `scrollTo` and `render`
 */