@xterm/addon-image 0.10.0-beta.18 → 0.10.0-beta.180
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/addon-image.js +1 -1
- package/lib/addon-image.js.map +1 -1
- package/lib/addon-image.mjs +1 -22
- package/lib/addon-image.mjs.map +4 -4
- package/package.json +5 -5
- package/src/IIPHandler.ts +32 -21
- package/src/IIPHeaderParser.ts +3 -2
- package/src/IIPImageStorage.ts +26 -0
- package/src/ImageAddon.ts +30 -5
- package/src/ImageRenderer.ts +91 -47
- package/src/ImageStorage.ts +128 -64
- package/src/SixelHandler.ts +3 -3
- package/src/SixelImageStorage.ts +50 -0
- package/src/Types.ts +8 -2
- package/src/kitty/KittyGraphicsHandler.ts +820 -0
- package/src/kitty/KittyGraphicsTypes.ts +193 -0
- package/src/kitty/KittyImageStorage.ts +151 -0
- package/typings/addon-image.d.ts +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xterm/addon-image",
|
|
3
|
-
"version": "0.10.0-beta.
|
|
3
|
+
"version": "0.10.0-beta.180",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "The xterm.js authors",
|
|
6
6
|
"url": "https://xtermjs.org/"
|
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
"xterm.js"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"prepackage": "../../node_modules/.bin/
|
|
21
|
+
"prepackage": "../../node_modules/.bin/tsgo -p .",
|
|
22
22
|
"package": "../../node_modules/.bin/webpack",
|
|
23
23
|
"prepublishOnly": "npm run package",
|
|
24
24
|
"start": "node ../../demo/start"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"sixel": "^0.16.0",
|
|
28
|
-
"xterm-wasm-parts": "^0.
|
|
28
|
+
"xterm-wasm-parts": "^0.3.0"
|
|
29
29
|
},
|
|
30
|
-
"commit": "
|
|
30
|
+
"commit": "783c4a34d296480f29ecddfb8743e4e542e29cf5",
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@xterm/xterm": "^6.1.0-beta.
|
|
32
|
+
"@xterm/xterm": "^6.1.0-beta.180"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/IIPHandler.ts
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
|
6
6
|
import { ImageRenderer } from './ImageRenderer';
|
|
7
|
-
import {
|
|
7
|
+
import { IIPImageStorage } from './IIPImageStorage';
|
|
8
|
+
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
8
9
|
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
|
9
10
|
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
|
10
11
|
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
13
|
+
// Local const enum mirror - esbuild can't inline const enums from external packages
|
|
14
|
+
const enum DecoderConst {
|
|
15
|
+
// Limit held memory in base64 decoder (encoded bytes).
|
|
16
|
+
KEEP_DATA = 4194304,
|
|
17
|
+
// Initial buffer allocation for the decoder.
|
|
18
|
+
INITIAL_DATA = 1048576,
|
|
19
|
+
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
|
20
|
+
OK = 0
|
|
21
|
+
}
|
|
18
22
|
|
|
19
23
|
// default IIP header values
|
|
20
24
|
const DEFAULT_HEADER: IHeaderFields = {
|
|
@@ -31,15 +35,19 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
31
35
|
private _aborted = false;
|
|
32
36
|
private _hp = new HeaderParser();
|
|
33
37
|
private _header: IHeaderFields = DEFAULT_HEADER;
|
|
34
|
-
private _dec
|
|
38
|
+
private _dec: Base64Decoder;
|
|
35
39
|
private _metrics = UNSUPPORTED_TYPE;
|
|
36
40
|
|
|
37
41
|
constructor(
|
|
38
42
|
private readonly _opts: IImageAddonOptions,
|
|
39
43
|
private readonly _renderer: ImageRenderer,
|
|
40
|
-
private readonly _storage:
|
|
44
|
+
private readonly _storage: IIPImageStorage,
|
|
41
45
|
private readonly _coreTerminal: ITerminalExt
|
|
42
|
-
) {
|
|
46
|
+
) {
|
|
47
|
+
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
|
48
|
+
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
|
|
49
|
+
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
|
|
50
|
+
}
|
|
43
51
|
|
|
44
52
|
public reset(): void {}
|
|
45
53
|
|
|
@@ -54,7 +62,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
54
62
|
if (this._aborted) return;
|
|
55
63
|
|
|
56
64
|
if (this._hp.state === HeaderState.END) {
|
|
57
|
-
if (this._dec.put(data
|
|
65
|
+
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
|
|
58
66
|
this._dec.release();
|
|
59
67
|
this._aborted = true;
|
|
60
68
|
}
|
|
@@ -70,8 +78,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
70
78
|
this._aborted = true;
|
|
71
79
|
return;
|
|
72
80
|
}
|
|
73
|
-
this._dec.init(
|
|
74
|
-
if (this._dec.put(data
|
|
81
|
+
this._dec.init();
|
|
82
|
+
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
|
|
75
83
|
this._dec.release();
|
|
76
84
|
this._aborted = true;
|
|
77
85
|
}
|
|
@@ -89,13 +97,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
89
97
|
let cond: number | boolean = true;
|
|
90
98
|
if (cond = success) {
|
|
91
99
|
if (cond = !this._dec.end()) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
if (cond = this._dec.data8.length === this._header.size) {
|
|
101
|
+
this._metrics = imageType(this._dec.data8);
|
|
102
|
+
if (cond = this._metrics.mime !== 'unsupported') {
|
|
103
|
+
w = this._metrics.width;
|
|
104
|
+
h = this._metrics.height;
|
|
105
|
+
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
|
106
|
+
[w, h] = this._resize(w, h).map(Math.floor);
|
|
107
|
+
cond = w && h && w * h < this._opts.pixelLimit;
|
|
108
|
+
}
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
111
|
}
|
|
@@ -105,7 +115,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
105
115
|
return true;
|
|
106
116
|
}
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
// HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
|
|
119
|
+
const blob = new Blob([this._dec.data8 as Uint8Array<ArrayBuffer>], { type: this._metrics.mime });
|
|
109
120
|
this._dec.release();
|
|
110
121
|
|
|
111
122
|
if (!window.createImageBitmap) {
|
package/src/IIPHeaderParser.ts
CHANGED
|
@@ -8,6 +8,7 @@ declare const Buffer: any;
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
export interface IHeaderFields {
|
|
11
|
+
[key: string]: number | string | Uint32Array | null | undefined;
|
|
11
12
|
// base-64 encoded filename. Defaults to "Unnamed file".
|
|
12
13
|
name: string;
|
|
13
14
|
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
|
@@ -81,7 +82,7 @@ function toName(data: Uint32Array): string {
|
|
|
81
82
|
return new TextDecoder().decode(b);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
const DECODERS: {[key: string]: (v: Uint32Array) =>
|
|
85
|
+
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
|
|
85
86
|
inline: toInt,
|
|
86
87
|
size: toInt,
|
|
87
88
|
name: toName,
|
|
@@ -100,7 +101,7 @@ export class HeaderParser {
|
|
|
100
101
|
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
|
101
102
|
private _position = 0;
|
|
102
103
|
private _key = '';
|
|
103
|
-
public fields: {[key: string]:
|
|
104
|
+
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
|
|
104
105
|
|
|
105
106
|
public reset(): void {
|
|
106
107
|
this._buffer.fill(0);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ImageStorage } from './ImageStorage';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* IIP (iTerm Image Protocol) specific image storage controller.
|
|
10
|
+
*
|
|
11
|
+
* Wraps the shared ImageStorage with IIP protocol semantics:
|
|
12
|
+
* - Always uses scrolling mode (cursor advances with image)
|
|
13
|
+
*/
|
|
14
|
+
export class IIPImageStorage {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly _storage: ImageStorage
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add an IIP image to storage.
|
|
21
|
+
* Always uses scrolling mode — cursor advances past the image.
|
|
22
|
+
*/
|
|
23
|
+
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
|
24
|
+
this._storage.addImage(img, true);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/ImageAddon.ts
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ITerminalAddon, IDisposable } from '@xterm/xterm';
|
|
7
7
|
import type { ImageAddon as IImageApi } from '@xterm/addon-image';
|
|
8
|
+
import { Emitter, type IEvent } from 'common/Event';
|
|
8
9
|
import { IIPHandler } from './IIPHandler';
|
|
9
10
|
import { ImageRenderer } from './ImageRenderer';
|
|
10
11
|
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
12
|
+
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
|
|
13
|
+
import { KittyImageStorage } from './kitty/KittyImageStorage';
|
|
11
14
|
import { SixelHandler } from './SixelHandler';
|
|
15
|
+
import { SixelImageStorage } from './SixelImageStorage';
|
|
16
|
+
import { IIPImageStorage } from './IIPImageStorage';
|
|
12
17
|
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
|
13
18
|
|
|
14
19
|
// default values of addon ctor options
|
|
@@ -22,7 +27,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
|
|
|
22
27
|
storageLimit: 128,
|
|
23
28
|
showPlaceholder: true,
|
|
24
29
|
iipSupport: true,
|
|
25
|
-
iipSizeLimit: 20000000
|
|
30
|
+
iipSizeLimit: 20000000,
|
|
31
|
+
kittySupport: true,
|
|
32
|
+
kittySizeLimit: 20000000
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
// max palette size supported by the sixel lib (compile time setting)
|
|
@@ -48,7 +55,7 @@ const enum GaStatus {
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
|
|
51
|
-
export class ImageAddon implements ITerminalAddon
|
|
58
|
+
export class ImageAddon implements ITerminalAddon, IImageApi {
|
|
52
59
|
private _opts: IImageAddonOptions;
|
|
53
60
|
private _defaultOpts: IImageAddonOptions;
|
|
54
61
|
private _storage: ImageStorage | undefined;
|
|
@@ -56,6 +63,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
56
63
|
private _disposables: IDisposable[] = [];
|
|
57
64
|
private _terminal: ITerminalExt | undefined;
|
|
58
65
|
private _handlers: Map<String, IResetHandler> = new Map();
|
|
66
|
+
private readonly _onImageAdded = new Emitter<void>();
|
|
67
|
+
public readonly onImageAdded: IEvent<void> = this._onImageAdded.event;
|
|
59
68
|
|
|
60
69
|
constructor(opts?: Partial<IImageAddonOptions>) {
|
|
61
70
|
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
|
|
@@ -68,6 +77,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
68
77
|
}
|
|
69
78
|
this._disposables.length = 0;
|
|
70
79
|
this._handlers.clear();
|
|
80
|
+
this._onImageAdded.dispose();
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
private _disposeLater(...args: IDisposable[]): void {
|
|
@@ -82,6 +92,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
82
92
|
// internal data structures
|
|
83
93
|
this._renderer = new ImageRenderer(terminal);
|
|
84
94
|
this._storage = new ImageStorage(terminal, this._renderer, this._opts);
|
|
95
|
+
this._storage.onImageAdded = () => this._onImageAdded.fire();
|
|
85
96
|
|
|
86
97
|
// enable size reports
|
|
87
98
|
if (this._opts.enableSizeReports) {
|
|
@@ -90,7 +101,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
90
101
|
// windowOptions.getCellSizePixels = true;
|
|
91
102
|
// windowOptions.getWinSizeChars = true;
|
|
92
103
|
// terminal.setOption('windowOptions', windowOptions);
|
|
93
|
-
const windowOps = terminal.options.windowOptions
|
|
104
|
+
const windowOps = terminal.options.windowOptions ?? {};
|
|
94
105
|
windowOps.getWinSizePixels = true;
|
|
95
106
|
windowOps.getCellSizePixels = true;
|
|
96
107
|
windowOps.getWinSizeChars = true;
|
|
@@ -129,7 +140,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
129
140
|
|
|
130
141
|
// SIXEL handler
|
|
131
142
|
if (this._opts.sixelSupport) {
|
|
132
|
-
const
|
|
143
|
+
const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
|
|
144
|
+
const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
|
|
133
145
|
this._handlers.set('sixel', sixelHandler);
|
|
134
146
|
this._disposeLater(
|
|
135
147
|
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
|
|
@@ -138,12 +150,25 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
138
150
|
|
|
139
151
|
// iTerm IIP handler
|
|
140
152
|
if (this._opts.iipSupport) {
|
|
141
|
-
const
|
|
153
|
+
const iipStorage = new IIPImageStorage(this._storage!);
|
|
154
|
+
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
|
|
142
155
|
this._handlers.set('iip', iipHandler);
|
|
143
156
|
this._disposeLater(
|
|
144
157
|
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
|
145
158
|
);
|
|
146
159
|
}
|
|
160
|
+
|
|
161
|
+
// Kitty graphics handler
|
|
162
|
+
if (this._opts.kittySupport) {
|
|
163
|
+
const kittyStorage = new KittyImageStorage(this._storage!);
|
|
164
|
+
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
|
|
165
|
+
this._handlers.set('kitty', kittyHandler);
|
|
166
|
+
this._disposeLater(
|
|
167
|
+
kittyStorage,
|
|
168
|
+
kittyHandler,
|
|
169
|
+
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
147
172
|
}
|
|
148
173
|
|
|
149
174
|
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
package/src/ImageRenderer.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { toRGBA8888 } from 'sixel/lib/Colors';
|
|
7
7
|
import { IDisposable } from '@xterm/xterm';
|
|
8
|
-
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
|
9
|
-
import { Disposable, MutableDisposable, toDisposable } from '
|
|
8
|
+
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
|
9
|
+
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
|
10
10
|
|
|
11
11
|
const PLACEHOLDER_LENGTH = 4096;
|
|
12
12
|
const PLACEHOLDER_HEIGHT = 24;
|
|
@@ -18,8 +18,9 @@ const PLACEHOLDER_HEIGHT = 24;
|
|
|
18
18
|
* - draw image tiles onRender
|
|
19
19
|
*/
|
|
20
20
|
export class ImageRenderer extends Disposable implements IDisposable {
|
|
21
|
-
|
|
22
|
-
|
|
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>();
|
|
23
24
|
private _placeholder: HTMLCanvasElement | undefined;
|
|
24
25
|
private _placeholderBitmap: ImageBitmap | undefined;
|
|
25
26
|
private _optionsRefresh = this._register(new MutableDisposable());
|
|
@@ -38,7 +39,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
38
39
|
* Only the DOM output canvas should be on the terminal's document,
|
|
39
40
|
* which gets explicitly checked in `insertLayerToDom`.
|
|
40
41
|
*/
|
|
41
|
-
const canvas = (localDocument
|
|
42
|
+
const canvas = (localDocument ?? document).createElement('canvas');
|
|
42
43
|
canvas.width = width | 0;
|
|
43
44
|
canvas.height = height | 0;
|
|
44
45
|
return canvas;
|
|
@@ -86,6 +87,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
86
87
|
});
|
|
87
88
|
this._register(toDisposable(() => {
|
|
88
89
|
this.removeLayerFromDom();
|
|
90
|
+
this.removeLayerFromDom('bottom');
|
|
89
91
|
if (this._terminal._core && this._oldOpen) {
|
|
90
92
|
this._terminal._core.open = this._oldOpen;
|
|
91
93
|
this._oldOpen = undefined;
|
|
@@ -95,8 +97,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
95
97
|
this._oldSetRenderer = undefined;
|
|
96
98
|
}
|
|
97
99
|
this._renderService = undefined;
|
|
98
|
-
this.
|
|
99
|
-
this._ctx = undefined;
|
|
100
|
+
this._layers.clear();
|
|
100
101
|
this._placeholderBitmap?.close();
|
|
101
102
|
this._placeholderBitmap = undefined;
|
|
102
103
|
this._placeholder = undefined;
|
|
@@ -124,7 +125,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
124
125
|
* Forwarded from internal render service.
|
|
125
126
|
*/
|
|
126
127
|
public get dimensions(): IRenderDimensions | undefined {
|
|
127
|
-
return this.
|
|
128
|
+
return this._terminal.dimensions;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
/**
|
|
@@ -140,27 +141,38 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
140
141
|
/**
|
|
141
142
|
* Clear a region of the image layer canvas.
|
|
142
143
|
*/
|
|
143
|
-
public clearLines(start: number, end: number): void {
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(
|
|
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
|
+
}
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
/**
|
|
153
157
|
* Clear whole image canvas.
|
|
154
158
|
*/
|
|
155
|
-
public clearAll(): void {
|
|
156
|
-
|
|
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
|
+
}
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
/**
|
|
160
171
|
* Draw neighboring tiles on the image layer canvas.
|
|
161
172
|
*/
|
|
162
173
|
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
|
163
|
-
|
|
174
|
+
const ctx = this._layers.get(imgSpec.layer);
|
|
175
|
+
if (!ctx) {
|
|
164
176
|
return;
|
|
165
177
|
}
|
|
166
178
|
const { width, height } = this.cellSize;
|
|
@@ -187,7 +199,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
187
199
|
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
|
188
200
|
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
|
189
201
|
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
|
190
|
-
|
|
202
|
+
ctx.drawImage(
|
|
191
203
|
img,
|
|
192
204
|
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
|
193
205
|
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
|
@@ -227,7 +239,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
227
239
|
* Draw a line with placeholder on the image layer canvas.
|
|
228
240
|
*/
|
|
229
241
|
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
|
230
|
-
|
|
242
|
+
const ctx = this._layers.get('top');
|
|
243
|
+
if (ctx) {
|
|
231
244
|
const { width, height } = this.cellSize;
|
|
232
245
|
|
|
233
246
|
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
@@ -241,8 +254,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
241
254
|
this._createPlaceHolder(height + 1);
|
|
242
255
|
}
|
|
243
256
|
if (!this._placeholder) return;
|
|
244
|
-
|
|
245
|
-
this._placeholderBitmap
|
|
257
|
+
ctx.drawImage(
|
|
258
|
+
this._placeholderBitmap ?? this._placeholder!,
|
|
246
259
|
col * width,
|
|
247
260
|
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
|
248
261
|
width * count,
|
|
@@ -260,12 +273,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
260
273
|
* Checked once from `ImageStorage.render`.
|
|
261
274
|
*/
|
|
262
275
|
public rescaleCanvas(): void {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
}
|
|
269
283
|
}
|
|
270
284
|
}
|
|
271
285
|
|
|
@@ -304,37 +318,67 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
304
318
|
this._renderService = this._terminal._core._renderService;
|
|
305
319
|
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
|
|
306
320
|
this._renderService.setRenderer = (renderer: any) => {
|
|
307
|
-
this.
|
|
321
|
+
for (const key of [...this._layers.keys()]) {
|
|
322
|
+
this.removeLayerFromDom(key);
|
|
323
|
+
}
|
|
308
324
|
this._oldSetRenderer?.call(this._renderService, renderer);
|
|
309
325
|
};
|
|
310
326
|
}
|
|
311
327
|
|
|
312
|
-
public insertLayerToDom(): void {
|
|
328
|
+
public insertLayerToDom(layer: ImageLayer = 'top'): void {
|
|
313
329
|
// make sure that the terminal is attached to a document and to DOM
|
|
314
|
-
if (this.document
|
|
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) {
|
|
326
331
|
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;
|
|
327
336
|
}
|
|
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
|
+
// Use isolation to create a stacking context without overriding z-index,
|
|
344
|
+
// which would conflict with integrators (e.g. VS Code) that set their
|
|
345
|
+
// own z-index on the screen element.
|
|
346
|
+
screenElement.style.isolation = 'isolate';
|
|
347
|
+
if (layer === 'bottom') {
|
|
348
|
+
// Use z-index:-1 so it paints behind non-positioned text elements.
|
|
349
|
+
// The screen element needs to be a stacking context (via isolation)
|
|
350
|
+
// to contain the negative z-index, otherwise it would go behind the
|
|
351
|
+
// entire terminal.
|
|
352
|
+
canvas.style.zIndex = '-1';
|
|
353
|
+
screenElement.insertBefore(canvas, screenElement.firstChild);
|
|
354
|
+
} else {
|
|
355
|
+
// Explicit z-index ensures the image canvas reliably stacks above
|
|
356
|
+
// the text layer (DOM renderer rows). z-index: 0 is below the
|
|
357
|
+
// selection overlay (z-index: 1).
|
|
358
|
+
canvas.style.zIndex = '0';
|
|
359
|
+
screenElement.appendChild(canvas);
|
|
360
|
+
}
|
|
361
|
+
const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
|
|
362
|
+
if (!ctx) {
|
|
363
|
+
canvas.remove();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this._layers.set(layer, ctx);
|
|
367
|
+
this.clearAll(layer);
|
|
328
368
|
}
|
|
329
369
|
|
|
330
|
-
public removeLayerFromDom(): void {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.
|
|
370
|
+
public removeLayerFromDom(layer: ImageLayer = 'top'): void {
|
|
371
|
+
const ctx = this._layers.get(layer);
|
|
372
|
+
if (ctx) {
|
|
373
|
+
ctx.canvas.remove();
|
|
374
|
+
this._layers.delete(layer);
|
|
335
375
|
}
|
|
336
376
|
}
|
|
337
377
|
|
|
378
|
+
public hasLayer(layer: ImageLayer): boolean {
|
|
379
|
+
return this._layers.has(layer);
|
|
380
|
+
}
|
|
381
|
+
|
|
338
382
|
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
|
339
383
|
this._placeholderBitmap?.close();
|
|
340
384
|
this._placeholderBitmap = undefined;
|