Skip to content

Commit 21f5156

Browse files
committed
Fix image layer stacking + tile survival across text write
1 parent 27b01b2 commit 21f5156

File tree

4 files changed

+51
-35
lines changed

4 files changed

+51
-35
lines changed

addons/addon-image/src/ImageRenderer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ export class ImageRenderer extends Disposable implements IDisposable {
348348
screenElement.style.zIndex = '0';
349349
screenElement.insertBefore(canvas, screenElement.firstChild);
350350
} 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';
351356
screenElement.appendChild(canvas);
352357
}
353358
const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });

addons/addon-image/src/ImageStorage.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -415,46 +415,56 @@ export class ImageStorage implements IDisposable {
415415
const placeholderCalls: { col: number, row: number, count: number }[] = [];
416416

417417
// 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).
418421
for (let row = start; row <= end; ++row) {
419422
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
420423
if (!line) return;
421424
for (let col = 0; col < cols; ++col) {
425+
let e: IExtendedAttrsImage;
422426
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
423-
let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS;
424-
const imageId = e.imageId;
425-
if (imageId === undefined || imageId === -1) {
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) {
426431
continue;
427432
}
428-
const imgSpec = this._images.get(imageId);
429-
if (e.tileId !== -1) {
430-
const startTile = e.tileId;
431-
const startCol = col;
432-
let count = 1;
433-
/**
434-
* merge tiles to the right into a single draw call, if:
435-
* - not at end of line
436-
* - cell has same image id
437-
* - cell has consecutive tile id
438-
*/
439-
while (
440-
++col < cols
441-
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
442-
&& (e = line._extendedAttrs[col] ?? EMPTY_ATTRS)
443-
&& (e.imageId === imageId)
444-
&& (e.tileId === startTile + count)
445-
) {
446-
count++;
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;
447455
}
448-
col--;
449-
if (imgSpec) {
450-
if (imgSpec.actual) {
451-
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
452-
}
453-
} else if (this._opts.showPlaceholder) {
454-
placeholderCalls.push({ col: startCol, row, count });
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 });
455463
}
456-
this._fullyCleared = false;
464+
} else if (this._opts.showPlaceholder) {
465+
placeholderCalls.push({ col: startCol, row, count });
457466
}
467+
this._fullyCleared = false;
458468
}
459469
}
460470
}

addons/addon-image/src/kitty/KittyGraphicsHandler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -571,10 +571,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
571571
const savedYbase = buffer.ybase;
572572

573573
// Determine layer based on z-index: negative = behind text, 0+ = on top.
574-
// Bottom layer only works when allowTransparency is enabled (otherwise text
575-
// canvas background is opaque and hides the bottom canvas). Fall back to top.
574+
// When z<0 we always use the bottom layer even without allowTransparency —
575+
// the image will simply be hidden behind the opaque text background, which
576+
// is the correct behavior (client asked for "behind text").
576577
const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0;
577-
const layer: ImageLayer = (wantsBottom && this._coreTerminal.options.allowTransparency) ? 'bottom' : 'top';
578+
const layer: ImageLayer = wantsBottom ? 'bottom' : 'top';
578579

579580
const zIndex = cmd.zIndex ?? 0;
580581
if (w !== bitmap.width || h !== bitmap.height) {

addons/addon-image/test/KittyGraphics.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,12 +1116,12 @@ test.describe('Kitty Graphics Protocol', () => {
11161116
strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), 1);
11171117
});
11181118

1119-
test('z=-1 falls back to top layer when allowTransparency is disabled', async () => {
1119+
test('z=-1 uses bottom layer even when allowTransparency is disabled', async () => {
11201120
await ctx.page.evaluate(`window.term.options.allowTransparency = false`);
11211121
await ctx.proxy.write(`\x1b_Ga=T,f=100,z=-1;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
11221122
await timeout(100);
11231123
strictEqual(await getImageStorageLength(), 1);
1124-
strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'top');
1124+
strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).layer`), 'bottom');
11251125
strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.get(1).zIndex`), -1);
11261126
});
11271127

0 commit comments

Comments
 (0)