From 677e5275f9fcecc39df90f0e19cbe0836efefb76 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 12 Feb 2026 10:27:32 -0800 Subject: [PATCH] Store z-index for drawing order --- addons/addon-image/src/ImageStorage.ts | 28 +++++++++++++++---- addons/addon-image/src/Types.ts | 1 + .../src/kitty/KittyGraphicsHandler.ts | 5 ++-- addons/addon-image/test/KittyGraphics.test.ts | 6 ++++ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index 3609b279..b06c7d59 100644 --- a/addons/addon-image/src/ImageStorage.ts +++ b/addons/addon-image/src/ImageStorage.ts @@ -250,7 +250,7 @@ export class ImageStorage implements IDisposable { * Method to add an image to the storage. * Returns the internal image ID assigned to the stored image. */ - public addImage(img: HTMLCanvasElement | ImageBitmap, layer: ImageLayer = 'top'): number { + public addImage(img: HTMLCanvasElement | ImageBitmap, layer: ImageLayer = 'top', zIndex: number = 0): number { // never allow storage to exceed memory limit this._evictOldest(img.width * img.height); @@ -340,7 +340,8 @@ export class ImageStorage implements IDisposable { marker: endMarker || undefined, tileCount, bufferType: this._terminal.buffer.active.type, - layer + layer, + zIndex }; // finally add the image @@ -419,7 +420,11 @@ export class ImageStorage implements IDisposable { // clear drawing area this._renderer.clearLines(start, end); - // walk all cells in viewport and draw tiles found + // Collect draw calls so we can sort by z-index (lower z drawn first). + const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = []; + const placeholderCalls: { col: number, row: number, count: number }[] = []; + + // walk all cells in viewport and collect tiles found for (let row = start; row <= end; ++row) { const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt; if (!line) return; @@ -453,16 +458,29 @@ export class ImageStorage implements IDisposable { col--; if (imgSpec) { if (imgSpec.actual) { - this._renderer.draw(imgSpec, startTile, startCol, row, count); + drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count }); } } else if (this._opts.showPlaceholder) { - this._renderer.drawPlaceholder(startCol, row, count); + placeholderCalls.push({ col: startCol, row, count }); } this._fullyCleared = false; } } } } + + // Sort by z-index so lower z draws first (higher z renders on top) + drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex); + + // Draw placeholders first (lowest priority) + for (const call of placeholderCalls) { + this._renderer.drawPlaceholder(call.col, call.row, call.count); + } + + // Draw images in z-index order + for (const call of drawCalls) { + this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count); + } } public viewportResize(metrics: { cols: number, rows: number }): void { diff --git a/addons/addon-image/src/Types.ts b/addons/addon-image/src/Types.ts index a9f3310b..80cec9e2 100644 --- a/addons/addon-image/src/Types.ts +++ b/addons/addon-image/src/Types.ts @@ -110,4 +110,5 @@ export interface IImageSpec { tileCount: number; bufferType: 'alternate' | 'normal'; layer: ImageLayer; + zIndex: number; } diff --git a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts index 8e3821f1..12caf59d 100644 --- a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts +++ b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts @@ -510,12 +510,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler { const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0; const layer: ImageLayer = (wantsBottom && this._coreTerminal.options.allowTransparency) ? 'bottom' : 'top'; + const zIndex = cmd.zIndex ?? 0; let storageId: number; if (w !== bitmap.width || h !== bitmap.height) { const resized = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h }); - storageId = this._storage.addImage(resized, layer); + storageId = this._storage.addImage(resized, layer, zIndex); } else { - storageId = this._storage.addImage(bitmap, layer); + storageId = this._storage.addImage(bitmap, layer, zIndex); } this._kittyIdToStorageId.set(image.id, storageId); diff --git a/addons/addon-image/test/KittyGraphics.test.ts b/addons/addon-image/test/KittyGraphics.test.ts index 38a70c17..5655660b 100644 --- a/addons/addon-image/test/KittyGraphics.test.ts +++ b/addons/addon-image/test/KittyGraphics.test.ts @@ -552,6 +552,7 @@ test.describe('Kitty Graphics Protocol', () => { await timeout(100); strictEqual(await getImageStorageLength(), 1); strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'top'); + strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), 0); }); test('z=0 stores image on top layer', async () => { @@ -559,6 +560,7 @@ test.describe('Kitty Graphics Protocol', () => { await timeout(100); strictEqual(await getImageStorageLength(), 1); strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'top'); + strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), 0); }); test('z=1 (positive) stores image on top layer', async () => { @@ -566,6 +568,7 @@ test.describe('Kitty Graphics Protocol', () => { await timeout(100); strictEqual(await getImageStorageLength(), 1); strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'top'); + strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), 1); }); test('z=-1 falls back to top layer when allowTransparency is disabled', async () => { @@ -574,6 +577,7 @@ test.describe('Kitty Graphics Protocol', () => { await timeout(100); strictEqual(await getImageStorageLength(), 1); strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'top'); + strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), -1); }); test('z=-1 (negative) stores image on bottom layer when allowTransparency is enabled', async () => { @@ -582,6 +586,7 @@ test.describe('Kitty Graphics Protocol', () => { await timeout(100); strictEqual(await getImageStorageLength(), 1); strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'bottom'); + strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), -1); }); test('z=-100 (large negative) stores image on bottom layer when allowTransparency is enabled', async () => { @@ -590,6 +595,7 @@ test.describe('Kitty Graphics Protocol', () => { await timeout(100); strictEqual(await getImageStorageLength(), 1); strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'bottom'); + strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), -100); }); test('top layer canvas has correct CSS class', async () => {