diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index 463a28ab9..c904eef9f 100644 --- a/addons/addon-image/src/ImageStorage.ts +++ b/addons/addon-image/src/ImageStorage.ts @@ -5,12 +5,11 @@ import { IDisposable } from '@xterm/xterm'; import { ImageRenderer } from './ImageRenderer'; -import { - ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, - IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, - UnderlineStyle, IAddImageOpts +import type { + ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts } from './Types'; - +import type { IBufferLine } from 'common/buffer/Types'; +import { CellData } from 'common/buffer/CellData'; // fallback default cell size export const CELL_SIZE_DEFAULT: ICellSize = { @@ -18,93 +17,12 @@ export const CELL_SIZE_DEFAULT: ICellSize = { height: 14 }; -/** - * Extend extended attribute to also hold image tile information. - * - * Object definition is copied from base repo to fully mimick its behavior. - * Image data is added as additional public properties `imageId` and `tileId`. - */ -class ExtendedAttrsImage implements IExtendedAttrsImage { - private _ext: number = 0; - public get ext(): number { - if (this._urlId) { - return ( - (this._ext & ~ExtFlags.UNDERLINE_STYLE) | - (this.underlineStyle << 26) - ); - } - return this._ext; - } - public set ext(value: number) { this._ext = value; } - - public get underlineStyle(): UnderlineStyle { - // Always return the URL style if it has one - if (this._urlId) { - return UnderlineStyle.DASHED; - } - return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26; - } - public set underlineStyle(value: UnderlineStyle) { - this._ext &= ~ExtFlags.UNDERLINE_STYLE; - this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE; - } - - public get underlineColor(): number { - return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK); - } - public set underlineColor(value: number) { - this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); - this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK); - } - - public get underlineVariantOffset(): number { - const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29; - if (val < 0) { - return val ^ 0xFFFFFFF8; - } - return val; - } - public set underlineVariantOffset(value: number) { - this._ext &= ~ExtFlags.VARIANT_OFFSET; - this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET; - } - - private _urlId: number = 0; - public get urlId(): number { - return this._urlId; - } - public set urlId(value: number) { - this._urlId = value; - } - +class ImageTileInfo { constructor( - ext: number = 0, - urlId: number = 0, public imageId = -1, - public tileId = -1 - ) { - this._ext = ext; - this._urlId = urlId; - } - - public clone(): IExtendedAttrsImage { - /** - * Technically we dont need a clone variant of ExtendedAttrsImage, - * as we never clone a cell holding image data. - * Note: Clone is only meant to be used by the InputHandler for - * sticky attributes, which is never the case for image data. - * We still provide a proper clone method to reflect the full ext attr - * state in case there are future use cases for clone. - */ - return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId); - } - - public isEmpty(): boolean { - return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1; + public tileId = -1) { } } -export const EMPTY_ATTRS = new ExtendedAttrsImage(); - /** * ImageStorage - extension of CoreTerminal: @@ -126,6 +44,7 @@ export class ImageStorage implements IDisposable { private _needsFullClear = false; // hard limit of stored pixels (fallback limit of 10 MB) private _pixelLimit: number = 2500000; + private _workCell: CellData = new CellData(); private _viewportMetrics: { cols: number, rows: number }; public onImageAdded: (() => void) | undefined; @@ -284,10 +203,10 @@ export class ImageStorage implements IDisposable { this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y); for (let row = 0; row < rows; ++row) { - const line = buffer.lines.get(buffer.y + buffer.ybase); + const line = buffer.lines.get(buffer.y + buffer.ybase)!; for (let col = 0; col < cols; ++col) { if (offset + col >= termCols) break; - this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col); + this._writeToCell(line, offset + col, imageId, row * cols + col); tileCount++; } if (opts.scrolling) { @@ -435,11 +354,11 @@ export class ImageStorage implements IDisposable { // 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; + const line = buffer.lines.get(row + buffer.ydisp); if (!line) return; for (let col = 0; col < cols; ++col) { - if (line.getBg(col) & BgFlags.HAS_EXTENDED) { - let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS; + const e = line.getExtended(col)?.payload; + if (e instanceof ImageTileInfo) { const imageId = e.imageId; if (imageId === undefined || imageId === -1) { continue; @@ -455,13 +374,11 @@ export class ImageStorage implements IDisposable { * - 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) - ) { + while (++col < cols) { + const nextE = line.getExtended(col)?.payload; + if (!(nextE instanceof ImageTileInfo) || nextE.imageId !== imageId || nextE.tileId !== startTile + count) { + break; + } count++; } col--; @@ -511,9 +428,9 @@ export class ImageStorage implements IDisposable { const rows = buffer.lines.length; const oldCol = this._viewportMetrics.cols - 1; for (let row = 0; row < rows; ++row) { - const line = buffer.lines.get(row) as IBufferLineExt; - if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS; + const line = buffer.lines.get(row)!; + const e = line.getExtended(oldCol)?.payload; + if (e instanceof ImageTileInfo) { const imageId = e.imageId; if (imageId === undefined || imageId === -1) { continue; @@ -530,7 +447,7 @@ export class ImageStorage implements IDisposable { // expand only if right side is empty (nothing got wrapped from below) let hasData = false; for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) { - if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) { + if (line.hasContent(rightCol)) { hasData = true; break; } @@ -542,7 +459,7 @@ export class ImageStorage implements IDisposable { const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol); let lastTile = e.tileId; for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) { - this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile); + this._writeToCell(line, expandCol, imageId, ++lastTile); imgSpec.tileCount++; } } @@ -556,10 +473,10 @@ export class ImageStorage implements IDisposable { */ public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined { const buffer = this._terminal._core.buffer; - const line = buffer.lines.get(y) as IBufferLineExt; - if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS; - if (e.imageId && e.imageId !== -1) { + const line = buffer.lines.get(y); + if (line) { + const e = line.getExtended(x)?.payload; + if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) { const orig = this._images.get(e.imageId)?.orig; if (window.ImageBitmap && orig instanceof ImageBitmap) { const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height); @@ -576,10 +493,10 @@ export class ImageStorage implements IDisposable { */ public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined { const buffer = this._terminal._core.buffer; - const line = buffer.lines.get(y) as IBufferLineExt; - if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { - const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS; - if (e.imageId && e.imageId !== -1 && e.tileId !== -1) { + const line = buffer.lines.get(y); + if (line) { + const e = line.getExtended(x)?.payload; + if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1 && e.tileId !== -1) { const spec = this._images.get(e.imageId); if (spec) { return this._renderer.extractTile(spec, e.tileId); @@ -607,31 +524,31 @@ export class ImageStorage implements IDisposable { return used - current; } - private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void { - if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { - const old = line._extendedAttrs[x]; - if (old) { - if (old.imageId !== undefined) { - // found an old ExtendedAttrsImage, since we know that - // they are always isolated instances (single cell usage), - // we can re-use it and just update their id entries - const oldSpec = this._images.get(old.imageId); - if (oldSpec) { - // early eviction for in-viewport overwrites - oldSpec.tileCount--; - } - old.imageId = imageId; - old.tileId = tileId; - return; + private _writeToCell(line: IBufferLine, x: number, imageId: number, tileId: number): void { + const workCell = this._workCell; + line.loadCell(x, workCell); + if (this._workCell.hasExtendedAttrs()) { + const old = workCell.extended.payload; + if (old instanceof ImageTileInfo) { + // found an old ExtendedAttrsImage, since we know that + // they are always isolated instances (single cell usage), + // we can re-use it and just update their id entries + const oldSpec = this._images.get(old.imageId); + if (oldSpec) { + // early eviction for in-viewport overwrites + oldSpec.tileCount--; } - // found a plain ExtendedAttrs instance, clone it to new entry - line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId); + old.imageId = imageId; + old.tileId = tileId; return; } } - // fall-through: always create new ExtendedAttrsImage entry - line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED; - line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId); + // fall-through: always create new ExtendedAttrs entry + const extattr = workCell.extended.clone(); + extattr.payload = new ImageTileInfo(imageId, tileId); + workCell.extended = extattr; + workCell.updateExtended(); + line.setCell(x, workCell); } private _evictOnAlternate(): void { @@ -644,18 +561,16 @@ export class ImageStorage implements IDisposable { // re-count tiles on whole buffer const buffer = this._terminal._core.buffer; for (let y = 0; y < this._terminal.rows; ++y) { - const line = buffer.lines.get(y) as IBufferLineExt; + const line = buffer.lines.get(y); if (!line) { continue; } for (let x = 0; x < this._terminal.cols; ++x) { - if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { - const imgId = line._extendedAttrs[x]?.imageId; - if (imgId) { - const spec = this._images.get(imgId); - if (spec) { - spec.tileCount++; - } + const payload = line.getExtended(x)?.payload; + if (payload instanceof ImageTileInfo) { + const spec = this._images.get(payload.imageId); + if (spec) { + spec.tileCount++; } } } diff --git a/addons/addon-image/src/Types.ts b/addons/addon-image/src/Types.ts index 229c335a9..cec840b0d 100644 --- a/addons/addon-image/src/Types.ts +++ b/addons/addon-image/src/Types.ts @@ -10,7 +10,6 @@ import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/b import type { AttributeData } from 'common/buffer/AttributeData'; import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types'; import type { IInputHandler } from 'common/Types'; -import type { IBufferLine, IExtendedAttrs } from 'common/buffer/Types'; import type { ITerminal, ReadonlyColorSet } from 'browser/Types'; import type { IRenderDimensions } from 'browser/renderer/shared/Types'; import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; @@ -50,6 +49,7 @@ export interface IResetHandler { reset(): void; } +/* eslint-disable */ /** * Stub into private interfaces. * This should be kept in line with common libs. @@ -57,19 +57,6 @@ export interface IResetHandler { * have a somewhat reliable testing against code changes in the core repo. */ -// overloaded IExtendedAttrs to hold image refs -export interface IExtendedAttrsImage extends IExtendedAttrs { - imageId: number; - tileId: number; - clone(): IExtendedAttrsImage; -} - -/* eslint-disable */ -export interface IBufferLineExt extends IBufferLine { - _extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined}; - _data: Uint32Array; -} - interface IInputHandlerExt extends IInputHandler { _parser: IEscapeSequenceParser; _curAttrData: AttributeData; @@ -80,6 +67,7 @@ interface IInputHandlerExt extends IInputHandler { }; onRequestReset(handler: () => void): IDisposable; } +/* eslint-enable */ export interface ICoreTerminalExt extends ITerminal { _themeService: IThemeService | undefined; @@ -91,8 +79,6 @@ export interface ICoreTerminalExt extends ITerminal { export interface ITerminalExt extends Terminal { _core: ICoreTerminalExt; } -/* eslint-enable */ - /** * Some storage definitions. diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 3bcb22a00..b37c15508 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -139,6 +139,7 @@ export class AttributeData implements IAttributeData { */ export class ExtendedAttrs implements IExtendedAttrs { private _ext: number = 0; + public payload: Object | undefined; public get ext(): number { if (this._urlId) { return ( @@ -207,6 +208,6 @@ export class ExtendedAttrs implements IExtendedAttrs { * that needs to be persistant in the buffer. */ public isEmpty(): boolean { - return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0; + return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.payload === undefined; } } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 4984f3a49..8166d1ff9 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -208,20 +208,25 @@ export class BufferLine implements IBufferLine { } else { cell.combinedData = ''; } - if (cell.bg & BgFlags.HAS_EXTENDED) { - cell.extended = this._extendedAttrs[index]!; - } else { - // Do not mutate cell.extended in place: it may still reference this line's map entry from a - // prior loadCell into a reused CellData (e.g. $workCell during insert/delete). - // We use $extended as blueprint and reset the internals - // mimicking the ctor to avoid a new allocation. - $extended._ext = 0; - $extended._urlId = 0; - cell.extended = $extended; - } + cell.extended = this.getExtended(index); return cell; } + public getExtended(index: number): IExtendedAttrs { + $startIndex = index * Constants.CELL_INDICIES; + if (this._data[$startIndex + Cell.BG] & BgFlags.HAS_EXTENDED) { + return this._extendedAttrs[index]!; + } + // Do not mutate cell.extended in place: it may still reference this line's map entry from a + // prior loadCell into a reused CellData (e.g. $workCell during insert/delete). + // We use $extended as blueprint and reset the internals + // mimicking the ctor to avoid a new allocation. + $extended._ext = 0; + $extended._urlId = 0; + $extended.payload = undefined; + return $extended; + } + /** * Set data at `index` to `cell`. */ diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.ts index a7cf0685a..025fed0b7 100644 --- a/src/common/buffer/Types.ts +++ b/src/common/buffer/Types.ts @@ -20,6 +20,7 @@ export interface IExtendedAttrs { underlineColor: number; underlineVariantOffset: number; urlId: number; + payload: Object | undefined; clone(): IExtendedAttrs; isEmpty(): boolean; } @@ -146,6 +147,7 @@ export interface IBufferLine { getCodePoint(index: number): number; isCombined(index: number): number; getString(index: number): string; + getExtended(index: number): IExtendedAttrs; } export interface IMarker extends IDisposable {