55
66import { toRGBA8888 } from 'sixel/lib/Colors' ;
77import { IDisposable } from '@xterm/xterm' ;
8- import { ICellSize , ITerminalExt , IImageSpec , IRenderDimensions , IRenderService } from './Types' ;
8+ import { ICellSize , ImageLayer , ITerminalExt , IImageSpec , IRenderDimensions , IRenderService } from './Types' ;
99import { Disposable , MutableDisposable , toDisposable } from 'common/Lifecycle' ;
1010
1111const PLACEHOLDER_LENGTH = 4096 ;
@@ -18,8 +18,9 @@ const PLACEHOLDER_HEIGHT = 24;
1818 * - draw image tiles onRender
1919 */
2020export class ImageRenderer extends Disposable implements IDisposable {
21- public canvas : HTMLCanvasElement | undefined ;
22- private _ctx : CanvasRenderingContext2D | null | undefined ;
21+ /** @deprecated Kept for backward compat — points to top layer canvas. */
22+ public get canvas ( ) : HTMLCanvasElement | undefined { return this . _layers . get ( 'top' ) ?. canvas ; }
23+ private _layers = new Map < ImageLayer , CanvasRenderingContext2D > ( ) ;
2324 private _placeholder : HTMLCanvasElement | undefined ;
2425 private _placeholderBitmap : ImageBitmap | undefined ;
2526 private _optionsRefresh = this . _register ( new MutableDisposable ( ) ) ;
@@ -86,6 +87,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
8687 } ) ;
8788 this . _register ( toDisposable ( ( ) => {
8889 this . removeLayerFromDom ( ) ;
90+ this . removeLayerFromDom ( 'bottom' ) ;
8991 if ( this . _terminal . _core && this . _oldOpen ) {
9092 this . _terminal . _core . open = this . _oldOpen ;
9193 this . _oldOpen = undefined ;
@@ -95,8 +97,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
9597 this . _oldSetRenderer = undefined ;
9698 }
9799 this . _renderService = undefined ;
98- this . canvas = undefined ;
99- this . _ctx = undefined ;
100+ this . _layers . clear ( ) ;
100101 this . _placeholderBitmap ?. close ( ) ;
101102 this . _placeholderBitmap = undefined ;
102103 this . _placeholder = undefined ;
@@ -140,27 +141,38 @@ export class ImageRenderer extends Disposable implements IDisposable {
140141 /**
141142 * Clear a region of the image layer canvas.
142143 */
143- public clearLines ( start : number , end : number ) : void {
144- this . _ctx ?. clearRect (
145- 0 ,
146- start * ( this . dimensions ?. css . cell . height || 0 ) ,
147- this . dimensions ?. css . canvas . width || 0 ,
148- ( ++ end - start ) * ( this . dimensions ?. css . cell . height || 0 )
149- ) ;
144+ public clearLines ( start : number , end : number , layer ?: ImageLayer ) : void {
145+ const y = start * ( this . dimensions ?. css . cell . height || 0 ) ;
146+ const w = this . dimensions ?. css . canvas . width || 0 ;
147+ const h = ( ++ end - start ) * ( this . dimensions ?. css . cell . height || 0 ) ;
148+ if ( ! layer || layer === 'top' ) {
149+ this . _layers . get ( 'top' ) ?. clearRect ( 0 , y , w , h ) ;
150+ }
151+ if ( ! layer || layer === 'bottom' ) {
152+ this . _layers . get ( 'bottom' ) ?. clearRect ( 0 , y , w , h ) ;
153+ }
150154 }
151155
152156 /**
153157 * Clear whole image canvas.
154158 */
155- public clearAll ( ) : void {
156- this . _ctx ?. clearRect ( 0 , 0 , this . canvas ?. width || 0 , this . canvas ?. height || 0 ) ;
159+ public clearAll ( layer ?: ImageLayer ) : void {
160+ if ( ! layer || layer === 'top' ) {
161+ const ctx = this . _layers . get ( 'top' ) ;
162+ ctx ?. clearRect ( 0 , 0 , ctx . canvas . width , ctx . canvas . height ) ;
163+ }
164+ if ( ! layer || layer === 'bottom' ) {
165+ const ctx = this . _layers . get ( 'bottom' ) ;
166+ ctx ?. clearRect ( 0 , 0 , ctx . canvas . width , ctx . canvas . height ) ;
167+ }
157168 }
158169
159170 /**
160171 * Draw neighboring tiles on the image layer canvas.
161172 */
162173 public draw ( imgSpec : IImageSpec , tileId : number , col : number , row : number , count : number = 1 ) : void {
163- if ( ! this . _ctx ) {
174+ const ctx = this . _layers . get ( imgSpec . layer ) ;
175+ if ( ! ctx ) {
164176 return ;
165177 }
166178 const { width, height } = this . cellSize ;
@@ -187,7 +199,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
187199 // Note: For not pixel perfect aligned cells like in the DOM renderer
188200 // this will move a tile slightly to the top/left (subpixel range, thus ignore it).
189201 // FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
190- this . _ctx . drawImage (
202+ ctx . drawImage (
191203 img ,
192204 Math . floor ( sx ) , Math . floor ( sy ) , Math . ceil ( finalWidth ) , Math . ceil ( finalHeight ) ,
193205 Math . floor ( dx ) , Math . floor ( dy ) , Math . ceil ( finalWidth ) , Math . ceil ( finalHeight )
@@ -227,7 +239,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
227239 * Draw a line with placeholder on the image layer canvas.
228240 */
229241 public drawPlaceholder ( col : number , row : number , count : number = 1 ) : void {
230- if ( this . _ctx ) {
242+ const ctx = this . _layers . get ( 'top' ) ;
243+ if ( ctx ) {
231244 const { width, height } = this . cellSize ;
232245
233246 // Don't try to draw anything, if we cannot get valid renderer metrics.
@@ -241,7 +254,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
241254 this . _createPlaceHolder ( height + 1 ) ;
242255 }
243256 if ( ! this . _placeholder ) return ;
244- this . _ctx . drawImage (
257+ ctx . drawImage (
245258 this . _placeholderBitmap ?? this . _placeholder ! ,
246259 col * width ,
247260 ( row * height ) % 2 ? 0 : 1 , // needs %2 offset correction
@@ -260,12 +273,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
260273 * Checked once from `ImageStorage.render`.
261274 */
262275 public rescaleCanvas ( ) : void {
263- if ( ! this . canvas ) {
264- return ;
265- }
266- if ( this . canvas . width !== this . dimensions ! . css . canvas . width || this . canvas . height !== this . dimensions ! . css . canvas . height ) {
267- this . canvas . width = this . dimensions ! . css . canvas . width || 0 ;
268- this . canvas . height = this . dimensions ! . css . canvas . height || 0 ;
276+ const w = this . dimensions ?. css . canvas . width || 0 ;
277+ const h = this . dimensions ?. css . canvas . height || 0 ;
278+ for ( const ctx of this . _layers . values ( ) ) {
279+ if ( ctx . canvas . width !== w || ctx . canvas . height !== h ) {
280+ ctx . canvas . width = w ;
281+ ctx . canvas . height = h ;
282+ }
269283 }
270284 }
271285
@@ -304,37 +318,64 @@ export class ImageRenderer extends Disposable implements IDisposable {
304318 this . _renderService = this . _terminal . _core . _renderService ;
305319 this . _oldSetRenderer = this . _renderService . setRenderer . bind ( this . _renderService ) ;
306320 this . _renderService . setRenderer = ( renderer : any ) => {
307- this . removeLayerFromDom ( ) ;
321+ for ( const key of [ ...this . _layers . keys ( ) ] ) {
322+ this . removeLayerFromDom ( key ) ;
323+ }
308324 this . _oldSetRenderer ?. call ( this . _renderService , renderer ) ;
309325 } ;
310326 }
311327
312- public insertLayerToDom ( ) : void {
328+ public insertLayerToDom ( layer : ImageLayer = 'top' ) : void {
313329 // make sure that the terminal is attached to a document and to DOM
314- if ( this . document && this . _terminal . _core . screenElement ) {
315- if ( ! this . canvas ) {
316- this . canvas = ImageRenderer . createCanvas (
317- this . document , this . dimensions ?. css . canvas . width || 0 ,
318- this . dimensions ?. css . canvas . height || 0
319- ) ;
320- this . canvas . classList . add ( 'xterm-image-layer' ) ;
321- this . _terminal . _core . screenElement . appendChild ( this . canvas ) ;
322- this . _ctx = this . canvas . getContext ( '2d' , { alpha : true , desynchronized : true } ) ;
323- this . clearAll ( ) ;
324- }
325- } else {
330+ if ( ! this . document || ! this . _terminal . _core . screenElement ) {
326331 console . warn ( 'image addon: cannot insert output canvas to DOM, missing document or screenElement' ) ;
332+ return ;
333+ }
334+ if ( this . _layers . has ( layer ) ) {
335+ return ;
327336 }
337+ const canvas = ImageRenderer . createCanvas (
338+ this . document , this . dimensions ?. css . canvas . width || 0 ,
339+ this . dimensions ?. css . canvas . height || 0
340+ ) ;
341+ canvas . classList . add ( `xterm-image-layer-${ layer } ` ) ;
342+ const screenElement = this . _terminal . _core . screenElement ;
343+ if ( layer === 'bottom' ) {
344+ // Use z-index:-1 so it paints behind non-positioned text elements.
345+ // The screen element needs to be a stacking context to contain the
346+ // negative z-index, otherwise it would go behind the entire terminal.
347+ canvas . style . zIndex = '-1' ;
348+ screenElement . style . zIndex = '0' ;
349+ screenElement . insertBefore ( canvas , screenElement . firstChild ) ;
350+ } else {
351+ // Explicit z-index ensures the image canvas reliably stacks above
352+ // the text layer (DOM renderer rows). z-index: 0 is below the
353+ // selection overlay (z-index: 1).
354+ canvas . style . zIndex = '0' ;
355+ screenElement . style . zIndex = '0' ;
356+ screenElement . appendChild ( canvas ) ;
357+ }
358+ const ctx = canvas . getContext ( '2d' , { alpha : true , desynchronized : true } ) ;
359+ if ( ! ctx ) {
360+ canvas . remove ( ) ;
361+ return ;
362+ }
363+ this . _layers . set ( layer , ctx ) ;
364+ this . clearAll ( layer ) ;
328365 }
329366
330- public removeLayerFromDom ( ) : void {
331- if ( this . canvas ) {
332- this . _ctx = undefined ;
333- this . canvas . remove ( ) ;
334- this . canvas = undefined ;
367+ public removeLayerFromDom ( layer : ImageLayer = 'top' ) : void {
368+ const ctx = this . _layers . get ( layer ) ;
369+ if ( ctx ) {
370+ ctx . canvas . remove ( ) ;
371+ this . _layers . delete ( layer ) ;
335372 }
336373 }
337374
375+ public hasLayer ( layer : ImageLayer ) : boolean {
376+ return this . _layers . has ( layer ) ;
377+ }
378+
338379 private _createPlaceHolder ( height : number = PLACEHOLDER_HEIGHT ) : void {
339380 this . _placeholderBitmap ?. close ( ) ;
340381 this . _placeholderBitmap = undefined ;
0 commit comments