@xterm/addon-image 0.10.0-beta.160 → 0.10.0-beta.161
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 -1
- package/lib/addon-image.mjs.map +4 -4
- package/package.json +3 -3
- package/src/ImageAddon.ts +17 -1
- package/src/ImageRenderer.ts +84 -43
- package/src/ImageStorage.ts +116 -43
- package/src/Types.ts +8 -2
- package/src/kitty/KittyGraphicsHandler.ts +721 -0
- package/src/kitty/KittyGraphicsTypes.ts +177 -0
- package/src/kitty/KittyImageStorage.ts +134 -0
- package/typings/addon-image.d.ts +9 -0
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.161",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "The xterm.js authors",
|
|
6
6
|
"url": "https://xtermjs.org/"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"sixel": "^0.16.0",
|
|
28
28
|
"xterm-wasm-parts": "^0.3.0"
|
|
29
29
|
},
|
|
30
|
-
"commit": "
|
|
30
|
+
"commit": "3a9bfa94bc41fb3f53b8926392d9cab854cab867",
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@xterm/xterm": "^6.1.0-beta.
|
|
32
|
+
"@xterm/xterm": "^6.1.0-beta.161"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/ImageAddon.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { ImageAddon as IImageApi } from '@xterm/addon-image';
|
|
|
8
8
|
import { IIPHandler } from './IIPHandler';
|
|
9
9
|
import { ImageRenderer } from './ImageRenderer';
|
|
10
10
|
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
11
|
+
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
|
|
12
|
+
import { KittyImageStorage } from './kitty/KittyImageStorage';
|
|
11
13
|
import { SixelHandler } from './SixelHandler';
|
|
12
14
|
import { SixelImageStorage } from './SixelImageStorage';
|
|
13
15
|
import { IIPImageStorage } from './IIPImageStorage';
|
|
@@ -24,7 +26,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
|
|
|
24
26
|
storageLimit: 128,
|
|
25
27
|
showPlaceholder: true,
|
|
26
28
|
iipSupport: true,
|
|
27
|
-
iipSizeLimit: 20000000
|
|
29
|
+
iipSizeLimit: 20000000,
|
|
30
|
+
kittySupport: true,
|
|
31
|
+
kittySizeLimit: 20000000
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
// max palette size supported by the sixel lib (compile time setting)
|
|
@@ -148,6 +152,18 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|
|
148
152
|
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
|
149
153
|
);
|
|
150
154
|
}
|
|
155
|
+
|
|
156
|
+
// Kitty graphics handler
|
|
157
|
+
if (this._opts.kittySupport) {
|
|
158
|
+
const kittyStorage = new KittyImageStorage(this._storage!);
|
|
159
|
+
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
|
|
160
|
+
this._handlers.set('kitty', kittyHandler);
|
|
161
|
+
this._disposeLater(
|
|
162
|
+
kittyStorage,
|
|
163
|
+
kittyHandler,
|
|
164
|
+
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
151
167
|
}
|
|
152
168
|
|
|
153
169
|
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
package/src/ImageRenderer.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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';
|
|
8
|
+
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
|
9
9
|
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
|
10
10
|
|
|
11
11
|
const PLACEHOLDER_LENGTH = 4096;
|
|
@@ -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());
|
|
@@ -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;
|
|
@@ -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,7 +254,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
241
254
|
this._createPlaceHolder(height + 1);
|
|
242
255
|
}
|
|
243
256
|
if (!this._placeholder) return;
|
|
244
|
-
|
|
257
|
+
ctx.drawImage(
|
|
245
258
|
this._placeholderBitmap ?? this._placeholder!,
|
|
246
259
|
col * width,
|
|
247
260
|
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
|
@@ -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,64 @@ 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
|
+
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);
|
|
328
365
|
}
|
|
329
366
|
|
|
330
|
-
public removeLayerFromDom(): void {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.
|
|
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);
|
|
335
372
|
}
|
|
336
373
|
}
|
|
337
374
|
|
|
375
|
+
public hasLayer(layer: ImageLayer): boolean {
|
|
376
|
+
return this._layers.has(layer);
|
|
377
|
+
}
|
|
378
|
+
|
|
338
379
|
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
|
339
380
|
this._placeholderBitmap?.close();
|
|
340
381
|
this._placeholderBitmap = undefined;
|
package/src/ImageStorage.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { IDisposable } from '@xterm/xterm';
|
|
7
7
|
import { ImageRenderer } from './ImageRenderer';
|
|
8
|
-
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
|
|
8
|
+
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
// fallback default cell size
|
|
@@ -124,6 +124,7 @@ export class ImageStorage implements IDisposable {
|
|
|
124
124
|
private _pixelLimit: number = 2500000;
|
|
125
125
|
|
|
126
126
|
private _viewportMetrics: { cols: number, rows: number };
|
|
127
|
+
public onImageDeleted: ((storageId: number) => void) | undefined;
|
|
127
128
|
|
|
128
129
|
constructor(
|
|
129
130
|
private _terminal: ITerminalExt,
|
|
@@ -189,11 +190,13 @@ export class ImageStorage implements IDisposable {
|
|
|
189
190
|
|
|
190
191
|
private _delImg(id: number): void {
|
|
191
192
|
const spec = this._images.get(id);
|
|
193
|
+
if (!spec) return;
|
|
192
194
|
this._images.delete(id);
|
|
193
195
|
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
|
194
|
-
if (
|
|
196
|
+
if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
|
195
197
|
spec.orig.close();
|
|
196
198
|
}
|
|
199
|
+
this.onImageDeleted?.(id);
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
/**
|
|
@@ -216,14 +219,28 @@ export class ImageStorage implements IDisposable {
|
|
|
216
219
|
this._fullyCleared = false;
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Delete an image by its internal storage ID.
|
|
224
|
+
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
|
225
|
+
*/
|
|
226
|
+
public deleteImage(id: number): void {
|
|
227
|
+
const spec = this._images.get(id);
|
|
228
|
+
if (spec) {
|
|
229
|
+
spec.marker?.dispose();
|
|
230
|
+
this._delImg(id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
/**
|
|
220
235
|
* Method to add an image to the storage.
|
|
221
236
|
* @param img - The image to add (canvas or bitmap).
|
|
222
237
|
* @param scrolling - When true, cursor advances with the image (lineFeed per row).
|
|
223
238
|
* When false, image is placed at (0,0) and cursor is restored (DECSET 80 / sixel origin mode).
|
|
239
|
+
* @param layer - Which canvas layer to render on ('top' or 'bottom').
|
|
240
|
+
* @param zIndex - Z-index for image layering within the same layer.
|
|
224
241
|
* @returns The internal image ID assigned to the stored image.
|
|
225
242
|
*/
|
|
226
|
-
public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean): number {
|
|
243
|
+
public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): number {
|
|
227
244
|
// never allow storage to exceed memory limit
|
|
228
245
|
this._evictOldest(img.width * img.height);
|
|
229
246
|
|
|
@@ -312,7 +329,9 @@ export class ImageStorage implements IDisposable {
|
|
|
312
329
|
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
|
313
330
|
marker: endMarker || undefined,
|
|
314
331
|
tileCount,
|
|
315
|
-
bufferType: this._terminal.buffer.active.type
|
|
332
|
+
bufferType: this._terminal.buffer.active.type,
|
|
333
|
+
layer,
|
|
334
|
+
zIndex
|
|
316
335
|
};
|
|
317
336
|
|
|
318
337
|
// finally add the image
|
|
@@ -327,16 +346,30 @@ export class ImageStorage implements IDisposable {
|
|
|
327
346
|
*/
|
|
328
347
|
// TODO: Should we move this to the ImageRenderer?
|
|
329
348
|
public render(range: { start: number, end: number }): void {
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
349
|
+
// Determine which layers have images
|
|
350
|
+
let hasTopImages = false;
|
|
351
|
+
let hasBottomImages = false;
|
|
352
|
+
for (const spec of this._images.values()) {
|
|
353
|
+
if (spec.layer === 'bottom') {
|
|
354
|
+
hasBottomImages = true;
|
|
355
|
+
} else {
|
|
356
|
+
hasTopImages = true;
|
|
336
357
|
}
|
|
358
|
+
if (hasTopImages && hasBottomImages) break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Lazily insert layers that are needed
|
|
362
|
+
if (hasTopImages && !this._renderer.hasLayer('top')) {
|
|
363
|
+
this._renderer.insertLayerToDom('top');
|
|
364
|
+
if (!this._renderer.hasLayer('top')) return;
|
|
337
365
|
}
|
|
366
|
+
if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
|
|
367
|
+
this._renderer.insertLayerToDom('bottom');
|
|
368
|
+
}
|
|
369
|
+
|
|
338
370
|
// rescale if needed
|
|
339
371
|
this._renderer.rescaleCanvas();
|
|
372
|
+
|
|
340
373
|
// exit early if we dont have any images to test for
|
|
341
374
|
if (!this._images.size) {
|
|
342
375
|
if (!this._fullyCleared) {
|
|
@@ -344,12 +377,25 @@ export class ImageStorage implements IDisposable {
|
|
|
344
377
|
this._fullyCleared = true;
|
|
345
378
|
this._needsFullClear = false;
|
|
346
379
|
}
|
|
347
|
-
if (this._renderer.
|
|
348
|
-
this._renderer.removeLayerFromDom();
|
|
380
|
+
if (this._renderer.hasLayer('top')) {
|
|
381
|
+
this._renderer.removeLayerFromDom('top');
|
|
382
|
+
}
|
|
383
|
+
if (this._renderer.hasLayer('bottom')) {
|
|
384
|
+
this._renderer.removeLayerFromDom('bottom');
|
|
349
385
|
}
|
|
350
386
|
return;
|
|
351
387
|
}
|
|
352
388
|
|
|
389
|
+
// Remove layers no longer needed
|
|
390
|
+
if (!hasTopImages && this._renderer.hasLayer('top')) {
|
|
391
|
+
this._renderer.clearAll('top');
|
|
392
|
+
this._renderer.removeLayerFromDom('top');
|
|
393
|
+
}
|
|
394
|
+
if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
|
|
395
|
+
this._renderer.clearAll('bottom');
|
|
396
|
+
this._renderer.removeLayerFromDom('bottom');
|
|
397
|
+
}
|
|
398
|
+
|
|
353
399
|
// buffer switches force a full clear
|
|
354
400
|
if (this._needsFullClear) {
|
|
355
401
|
this._renderer.clearAll();
|
|
@@ -364,50 +410,77 @@ export class ImageStorage implements IDisposable {
|
|
|
364
410
|
// clear drawing area
|
|
365
411
|
this._renderer.clearLines(start, end);
|
|
366
412
|
|
|
367
|
-
//
|
|
413
|
+
// Collect draw calls so we can sort by z-index (lower z drawn first).
|
|
414
|
+
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
|
|
415
|
+
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
|
416
|
+
|
|
417
|
+
// walk all cells in viewport and collect tiles found
|
|
418
|
+
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
|
|
419
|
+
// because text writes clear the BG flag but leave image tile data intact.
|
|
420
|
+
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
|
|
368
421
|
for (let row = start; row <= end; ++row) {
|
|
369
422
|
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
|
370
423
|
if (!line) return;
|
|
371
424
|
for (let col = 0; col < cols; ++col) {
|
|
425
|
+
let e: IExtendedAttrsImage;
|
|
372
426
|
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
427
|
+
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
|
428
|
+
} else {
|
|
429
|
+
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
|
430
|
+
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
|
|
376
431
|
continue;
|
|
377
432
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
433
|
+
e = maybeImg;
|
|
434
|
+
}
|
|
435
|
+
const imageId = e.imageId;
|
|
436
|
+
if (imageId === undefined || imageId === -1) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const imgSpec = this._images.get(imageId);
|
|
440
|
+
if (e.tileId !== -1) {
|
|
441
|
+
const startTile = e.tileId;
|
|
442
|
+
const startCol = col;
|
|
443
|
+
let count = 1;
|
|
444
|
+
/**
|
|
445
|
+
* merge tiles to the right into a single draw call, if:
|
|
446
|
+
* - not at end of line
|
|
447
|
+
* - cell has same image id
|
|
448
|
+
* - cell has consecutive tile id
|
|
449
|
+
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
|
|
450
|
+
*/
|
|
451
|
+
while (++col < cols) {
|
|
452
|
+
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
|
453
|
+
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
|
454
|
+
break;
|
|
397
455
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
456
|
+
e = nextE;
|
|
457
|
+
count++;
|
|
458
|
+
}
|
|
459
|
+
col--;
|
|
460
|
+
if (imgSpec) {
|
|
461
|
+
if (imgSpec.actual) {
|
|
462
|
+
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
|
405
463
|
}
|
|
406
|
-
|
|
464
|
+
} else if (this._opts.showPlaceholder) {
|
|
465
|
+
placeholderCalls.push({ col: startCol, row, count });
|
|
407
466
|
}
|
|
467
|
+
this._fullyCleared = false;
|
|
408
468
|
}
|
|
409
469
|
}
|
|
410
470
|
}
|
|
471
|
+
|
|
472
|
+
// Sort by z-index so lower z draws first (higher z renders on top)
|
|
473
|
+
drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex);
|
|
474
|
+
|
|
475
|
+
// Draw placeholders first (lowest priority)
|
|
476
|
+
for (const call of placeholderCalls) {
|
|
477
|
+
this._renderer.drawPlaceholder(call.col, call.row, call.count);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Draw images in z-index order
|
|
481
|
+
for (const call of drawCalls) {
|
|
482
|
+
this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count);
|
|
483
|
+
}
|
|
411
484
|
}
|
|
412
485
|
|
|
413
486
|
public viewportResize(metrics: { cols: number, rows: number }): void {
|
package/src/Types.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
|
|
|
8
8
|
// private imports from base repo we build against
|
|
9
9
|
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
|
10
10
|
import type { AttributeData } from 'common/buffer/AttributeData';
|
|
11
|
-
import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
|
11
|
+
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
|
12
12
|
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
|
|
13
13
|
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
|
14
14
|
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
|
@@ -22,7 +22,7 @@ export const enum Cell {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// export some privates for local usage
|
|
25
|
-
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
|
25
|
+
export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Plugin ctor options.
|
|
@@ -38,6 +38,8 @@ export interface IImageAddonOptions {
|
|
|
38
38
|
sixelSizeLimit: number;
|
|
39
39
|
iipSupport: boolean;
|
|
40
40
|
iipSizeLimit: number;
|
|
41
|
+
kittySupport: boolean;
|
|
42
|
+
kittySizeLimit: number;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface IResetHandler {
|
|
@@ -97,6 +99,8 @@ export interface ICellSize {
|
|
|
97
99
|
height: number;
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
export type ImageLayer = 'top' | 'bottom';
|
|
103
|
+
|
|
100
104
|
export interface IImageSpec {
|
|
101
105
|
orig: HTMLCanvasElement | ImageBitmap | undefined;
|
|
102
106
|
origCellSize: ICellSize;
|
|
@@ -105,4 +109,6 @@ export interface IImageSpec {
|
|
|
105
109
|
marker: IMarker | undefined;
|
|
106
110
|
tileCount: number;
|
|
107
111
|
bufferType: 'alternate' | 'normal';
|
|
112
|
+
layer: ImageLayer;
|
|
113
|
+
zIndex: number;
|
|
108
114
|
}
|