diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index 75851343c..322b9e02f 100644 --- a/addons/addon-image/src/ImageStorage.ts +++ b/addons/addon-image/src/ImageStorage.ts @@ -427,55 +427,46 @@ export class ImageStorage implements IDisposable { const placeholderCalls: { col: number, row: number, count: number }[] = []; // walk all cells in viewport and collect tiles found - // Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag) - // because text writes clear the BG flag but leave image tile data intact. - // This lets top-layer images survive text overwrites (kitty C=1 behavior). for (let row = start; row <= end; ++row) { const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt; if (!line) return; for (let col = 0; col < cols; ++col) { - let e: IExtendedAttrsImage; if (line.getBg(col) & BgFlags.HAS_EXTENDED) { - e = line._extendedAttrs[col] ?? EMPTY_ATTRS; - } else { - const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined; - if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) { + let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS; + const imageId = e.imageId; + if (imageId === undefined || imageId === -1) { continue; } - e = maybeImg; - } - const imageId = e.imageId; - if (imageId === undefined || imageId === -1) { - continue; - } - const imgSpec = this._images.get(imageId); - if (e.tileId !== -1) { - const startTile = e.tileId; - const startCol = col; - let count = 1; - /** - * merge tiles to the right into a single draw call, if: - * - not at end of line - * - cell has same image id - * - cell has consecutive tile id - * Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED. - */ - while (++col < cols) { - const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined; - if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) { - break; + const imgSpec = this._images.get(imageId); + if (e.tileId !== -1) { + const startTile = e.tileId; + const startCol = col; + let count = 1; + /** + * merge tiles to the right into a single draw call, if: + * - not at end of line + * - cell has same image id + * - cell has consecutive tile id + */ + while ( + ++col < cols + && (line.getBg(col) & BgFlags.HAS_EXTENDED) + && (e = line._extendedAttrs[col] ?? EMPTY_ATTRS) + && (e.imageId === imageId) + && (e.tileId === startTile + count) + ) { + count++; } - count++; - } - col--; - if (imgSpec) { - if (imgSpec.actual) { - drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count }); + col--; + if (imgSpec) { + if (imgSpec.actual) { + drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count }); + } + } else if (this._opts.showPlaceholder) { + placeholderCalls.push({ col: startCol, row, count }); } - } else if (this._opts.showPlaceholder) { - placeholderCalls.push({ col: startCol, row, count }); + this._fullyCleared = false; } - this._fullyCleared = false; } } } diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts index f3cde06a8..f4e54d7f2 100644 --- a/addons/addon-image/test/ImageAddon.test.ts +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -435,6 +435,15 @@ test.describe('ImageAddon', () => { }); } }); + + test.describe('text overwrite removes tiles', () => { + test('sixel', async () => { + await assertTextClearsTiles(SIXEL_SEQ_0); + }); + test('iip', async () => { + await assertTextClearsTiles(TESTDATA_IIP[0][0]); + }); + }); }); /** @@ -472,3 +481,20 @@ async function getOrigSize(id: number): Promise<[number, number]> { async function getImageAtBufferCell(x: number, y: number): Promise { return ctx.page.evaluate(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`); } + +async function hasTileAtBufferCell(x: number, y: number): Promise { + return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`); +} + +async function assertTextClearsTiles(imageSeq: string): Promise { + await ctx.proxy.write('\x1b[H' + imageSeq); + await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true); + ok(await hasTileAtBufferCell(0, 1)); + await ctx.proxy.write('\x1b[2;6H#######'); + for (let x = 5; x < 12; x++) { + strictEqual(await hasTileAtBufferCell(x, 1), false); + } + ok(await hasTileAtBufferCell(0, 1)); + ok(await hasTileAtBufferCell(13, 1)); + ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######')); +} diff --git a/addons/addon-image/test/KittyGraphics.test.ts b/addons/addon-image/test/KittyGraphics.test.ts index a7c8e60c1..8c1833454 100644 --- a/addons/addon-image/test/KittyGraphics.test.ts +++ b/addons/addon-image/test/KittyGraphics.test.ts @@ -2213,6 +2213,21 @@ test.describe('Kitty Graphics Protocol', () => { await pollFor(ctx.page, 'window._imageAddedCount', 2); }); }); + + test.describe('text overwrite removes tiles', () => { + test('a=T placement', async () => { + await ctx.proxy.write(`\x1b[H\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`); + await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true); + ok(await hasTileAtBufferCell(0, 1)); + await ctx.proxy.write('\x1b[2;6H#######'); + for (let x = 5; x < 12; x++) { + strictEqual(await hasTileAtBufferCell(x, 1), false); + } + ok(await hasTileAtBufferCell(0, 1)); + ok(await hasTileAtBufferCell(13, 1)); + ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######')); + }); + }); }); /** @@ -2262,3 +2277,7 @@ async function getPixels(col: number, row: number, x: number, y: number, w: numb return Array.from(ctx2d.getImageData(x, y, w, h).data); }, [col, row, x, y, w, h]); } + +async function hasTileAtBufferCell(x: number, y: number): Promise { + return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`); +}