import NeovimStore from './store' ; import * as A from './actions' ; import Cursor from './cursor' ; import Input from './input' ; import log from '../log' ; export default class NeovimScreen { ctx : CanvasRenderingContext2D ; cursor : Cursor ; input : Input ; constructor ( private readonly store , public canvas ) { this . ctx = this . canvas . getContext ( '2d' , { alpha : false } ) ; this . store . on ( 'put' , this . drawText . bind ( this ) ) ; this . store . on ( 'clear-all' , this . clearAll . bind ( this ) ) ; this . store . on ( 'clear-eol' , this . clearEol . bind ( this ) ) ; this . store . on ( 'update-bg' , this . clearAll . bind ( this ) ) ; this . store . on ( 'screen-scrolled' , this . scroll . bind ( this ) ) ; this . store . on ( 'line-height-changed' , ( ) => this . changeFontSize ( this . store . font_attr . specified_px ) ) ; this . changeFontSize ( this . store . font_attr . specified_px ) ; canvas . addEventListener ( 'click' , this . focus . bind ( this ) ) ; canvas . addEventListener ( 'mousedown' , this . mouseDown . bind ( this ) ) ; canvas . addEventListener ( 'mouseup' , this . mouseUp . bind ( this ) ) ; canvas . addEventListener ( 'mousemove' , this . mouseMove . bind ( this ) ) ; canvas . addEventListener ( 'wheel' , this . wheel . bind ( this ) ) ; this . cursor = new Cursor ( this . store , this . ctx ) ; this . input = new Input ( this . store ) ; } wheel ( e ) { this . store . dispatcher . dispatch ( A . wheelScroll ( e ) ) ; } mouseDown ( e ) { this . store . dispatcher . dispatch ( A . dragStart ( e ) ) ; } mouseUp ( e ) { this . store . dispatcher . dispatch ( A . dragEnd ( e ) ) ; } mouseMove ( e ) { if ( e . buttons !== 0 ) { this . store . dispatcher . dispatch ( A . dragUpdate ( e ) ) ; } } resizeWithPixels ( width_px , height_px ) { const res = window . devicePixelRatio || 1 ; const h = height_px * res ; const w = width_px * res ; this . resizeImpl ( Math . floor ( h / this . store . font_attr . draw_height ) , Math . floor ( w / this . store . font_attr . draw_width ) , w , h ) ; } resize ( lines , cols ) { this . resizeImpl ( lines , cols , this . store . font_attr . draw_width * cols , this . store . font_attr . draw_height * lines ) ; } changeFontSize ( specified_px ) { const res = window . devicePixelRatio || 1 ; const drawn_px = specified_px * res ; this . ctx . font = drawn_px + 'px ' + this . store . font_attr . face ; const font_width = this . ctx . measureText ( 'm' ) . width ; const font_height = Math . ceil ( drawn_px * this . store . line_height ) ; this . store . dispatcher . dispatch ( A . updateFontPx ( specified_px ) ) ; this . store . dispatcher . dispatch ( A . updateFontSize ( font_width , font_height , font_width / res , font_height / res ) ) ; const { width , height } = this . store . size ; this . resizeWithPixels ( width , height ) ; } changeLineHeight ( new_value ) { this . store . dispatcher . dispatch ( A . updateLineHeight ( new_value ) ) ; } scroll ( cols_delta ) { if ( cols_delta > 0 ) { this . scrollUp ( cols_delta ) ; } else if ( cols_delta < 0 ) { this . scrollDown ( - cols_delta ) ; } } focus ( ) { this . input . focus ( ) ; } clearAll ( ) { this . ctx . fillStyle = this . store . bg_color ; this . ctx . fillRect ( 0 , 0 , this . canvas . width , this . canvas . height ) ; } clearEol ( ) { const { line , col } = this . store . cursor ; const font_width = this . store . font_attr . draw_width ; const clear_length = this . store . size . cols * font_width - col * font_width ; log . debug ( `Clear until EOL: ${ line }:${ col } length=${ clear_length }` ) ; this . drawBlock ( line , col , 1 , clear_length , this . store . bg_color ) ; } convertPositionToLocation ( line , col ) { const { width , height } = this . store . font_attr ; return { x : col * width , y : line * height , } ; } convertLocationToPosition ( x , y ) { const { width , height } = this . store . font_attr ; return { line : Math . floor ( y * height ) , col : Math . floor ( x * width ) , } ; } checkShouldResize ( ) { const p = this . canvas . parentElement ; const cw = p . clientWidth ; const ch = p . clientHeight ; const w = this . canvas . width ; const h = this . canvas . height ; const res = window . devicePixelRatio || 1 ; if ( cw * res !== w || ch * res !== h ) { this . resizeWithPixels ( cw , ch ) ; } } private drawChars ( x , y , chars , width ) { let includes_half_only = true ; for ( const c of chars ) { if ( ! c [ 0 ] ) { includes_half_only = false ; break ; } } if ( includes_half_only ) { const text = chars . map ( c => c [ 0 ] || '' ) . join ( '' ) ; this . ctx . fillText ( text , x , y ) ; return ; } for ( const char of chars ) { if ( ! char [ 0 ] || char [ 0 ] === ' ' ) { x += width ; continue ; } this . ctx . fillText ( char . join ( '' ) , x , y ) ; x += width ; } } private drawText ( chars ) { const { line , col } = this . store . cursor ; const { fg , bg , sp , draw_width , draw_height , face , specified_px , bold , italic , underline , undercurl , } = this . store . font_attr ; this . drawBlock ( line , col , 1 , chars . length , bg ) ; const res = window . devicePixelRatio || 1 ; const font_size = specified_px * res ; let attrs = '' ; if ( bold ) { attrs += 'bold ' ; } if ( italic ) { attrs += 'italic ' ; } this . ctx . font = attrs + font_size + 'px ' + face ; this . ctx . textBaseline = 'top' ; this . ctx . fillStyle = fg ; const margin = Math . ceil ( ( font_size * ( this . store . line_height - 1.0 ) ) / 2 ) ; const y = Math . floor ( line * draw_height ) ; const x = col * draw_width ; this . drawChars ( x , y + margin , chars , draw_width ) ; if ( undercurl ) { this . ctx . strokeStyle = sp || this . store . sp_color || fg ; this . ctx . lineWidth = 1 * res ; this . ctx . setLineDash ( [ draw_width / 3 , draw_width / 3 ] ) ; this . ctx . beginPath ( ) ; const curl_y = y + draw_height - margin ; this . ctx . moveTo ( x , curl_y ) ; this . ctx . lineTo ( x + draw_width * chars . length , curl_y ) ; this . ctx . stroke ( ) ; } else if ( underline ) { this . ctx . strokeStyle = fg ; this . ctx . lineWidth = 1 * res ; this . ctx . setLineDash ( [ ] ) ; this . ctx . beginPath ( ) ; const underline_y = y + draw_height - margin ; this . ctx . moveTo ( x , underline_y ) ; this . ctx . lineTo ( x + draw_width * chars . length , underline_y ) ; this . ctx . stroke ( ) ; } log . debug ( `drawText(): (${ x }, ${ y })` , chars . length , this . store . cursor ) ; } private drawBlock ( line , col , height , width , color ) { const { draw_width , draw_height } = this . store . font_attr ; this . ctx . fillStyle = color ; this . ctx . fillRect ( Math . floor ( col * draw_width ) , line * draw_height , Math . ceil ( width * draw_width ) , height * draw_height ) ; } private slideVertical ( top , height , dst_top ) { const { left , right } = this . store . scroll_region ; const { draw_width , draw_height } = this . store . font_attr ; const x = left * draw_width ; const sy = top * draw_height ; const w = ( right - left + 1 ) * draw_width ; const h = height * draw_height ; const dy = dst_top * draw_height ; this . ctx . drawImage ( this . canvas , x , sy , w , h , x , dy , w , h ) ; } private scrollUp ( cols_up ) { const { top , bottom , left , right } = this . store . scroll_region ; this . slideVertical ( top + cols_up , bottom - ( top + cols_up ) + 1 , top ) ; this . drawBlock ( bottom - cols_up + 1 , left , cols_up , right - left + 1 , this . store . bg_color ) ; log . debug ( 'Scroll up: ' + cols_up , this . store . scroll_region ) ; } private scrollDown ( cols_down ) { const { top , bottom , left , right } = this . store . scroll_region ; this . slideVertical ( top , bottom - ( top + cols_down ) + 1 , top + cols_down ) ; this . drawBlock ( top , left , cols_down , right - left + 1 , this . store . bg_color ) ; log . debug ( 'Scroll down: ' + cols_down , this . store . scroll_region ) ; } private resizeImpl ( lines , cols , width , height ) { const res = window . devicePixelRatio || 1 ; if ( width !== this . canvas . width ) { this . canvas . width = width ; this . canvas . style . width = width / res + 'px' ; } if ( height !== this . canvas . height ) { this . canvas . height = height ; this . canvas . style . height = height / res + 'px' ; } this . store . dispatcher . dispatch ( A . updateScreenSize ( width , height ) ) ; this . store . dispatcher . dispatch ( A . updateScreenBounds ( lines , cols ) ) ; } }