Store z-index for drawing order

This commit is contained in:
Anthony Kim
2026-02-12 10:27:32 -08:00
parent d4c3b693bb
commit 677e5275f9
4 changed files with 33 additions and 7 deletions
+23 -5
View File
@@ -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 {
+1
View File
@@ -110,4 +110,5 @@ export interface IImageSpec {
tileCount: number;
bufferType: 'alternate' | 'normal';
layer: ImageLayer;
zIndex: number;
}
@@ -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);
@@ -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 () => {