Merge pull request #6128 from anthonykim1/anthonykim1/fix-pattern-glyph-owner-document
Fix pattern custom glyph rendering across documents
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { createPatternCanvas } from './CustomGlyphRasterizer';
|
||||
|
||||
describe('CustomGlyphRasterizer', () => {
|
||||
describe('createPatternCanvas', () => {
|
||||
it('prefers an offscreen canvas without using the DOM canvas factory', () => {
|
||||
const expectedCanvas = {} as OffscreenCanvas;
|
||||
const calls: string[] = [];
|
||||
|
||||
const canvas = createPatternCanvas(
|
||||
2,
|
||||
3,
|
||||
(width, height) => {
|
||||
calls.push(`offscreen:${width}x${height}`);
|
||||
return expectedCanvas;
|
||||
},
|
||||
() => {
|
||||
calls.push('dom');
|
||||
return {} as HTMLCanvasElement;
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual({ canvas, calls }, {
|
||||
canvas: expectedCanvas,
|
||||
calls: ['offscreen:2x3']
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the main-realm DOM canvas factory when OffscreenCanvas is unavailable', () => {
|
||||
const expectedCanvas = {} as HTMLCanvasElement;
|
||||
const calls: string[] = [];
|
||||
|
||||
const canvas = createPatternCanvas(
|
||||
4,
|
||||
5,
|
||||
undefined,
|
||||
(width, height) => {
|
||||
calls.push(`dom:${width}x${height}`);
|
||||
return expectedCanvas;
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual({ canvas, calls }, {
|
||||
canvas: expectedCanvas,
|
||||
calls: ['dom:4x5']
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,29 @@ import type { ILogService } from 'common/services/Services';
|
||||
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
||||
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||
|
||||
type PatternCanvas = HTMLCanvasElement | OffscreenCanvas;
|
||||
type PatternCanvasFactory = (width: number, height: number) => PatternCanvas;
|
||||
|
||||
const createOffscreenPatternCanvas: PatternCanvasFactory | undefined = typeof OffscreenCanvas === 'undefined'
|
||||
? undefined
|
||||
: (width, height) => new OffscreenCanvas(width, height);
|
||||
|
||||
const createDomPatternCanvas: PatternCanvasFactory = (width, height) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
};
|
||||
|
||||
export function createPatternCanvas(
|
||||
width: number,
|
||||
height: number,
|
||||
offscreenCanvasFactory: PatternCanvasFactory | undefined = createOffscreenPatternCanvas,
|
||||
domCanvasFactory: PatternCanvasFactory = createDomPatternCanvas
|
||||
): PatternCanvas {
|
||||
return offscreenCanvasFactory?.(width, height) ?? domCanvasFactory(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||
* successfully drawn.
|
||||
@@ -458,9 +481,9 @@ function drawPatternChar(
|
||||
if (!pattern) {
|
||||
const width = charDefinition[0].length;
|
||||
const height = charDefinition.length;
|
||||
const tmpCanvas = ctx.canvas.ownerDocument.createElement('canvas');
|
||||
tmpCanvas.width = width;
|
||||
tmpCanvas.height = height;
|
||||
// The atlas canvas can be adopted into another document, so temporary resources must not use
|
||||
// its mutable ownerDocument.
|
||||
const tmpCanvas = createPatternCanvas(width, height);
|
||||
const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
|
||||
const imageData = new ImageData(width, height);
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { platform } from 'os';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
|
||||
interface ITestRendererWithAtlasCanvas {
|
||||
readonly _charAtlas?: {
|
||||
readonly _tmpCanvas?: HTMLCanvasElement;
|
||||
};
|
||||
}
|
||||
|
||||
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||
const renderPromise = new Promise<void>(resolve => {
|
||||
const disposable = ctx.proxy.onRender(() => {
|
||||
disposable.dispose();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await ctx.proxy.write(data);
|
||||
await renderPromise;
|
||||
}
|
||||
|
||||
test.describe('WebGL custom glyphs', () => {
|
||||
if (platform() === 'linux') {
|
||||
test.skip(({ browserName }) => browserName === 'firefox' || browserName === 'webkit');
|
||||
}
|
||||
|
||||
test('pattern glyphs render after the terminal is adopted into another document', async ({ browser }) => {
|
||||
const ctx = await createTestContext(browser);
|
||||
const errors: string[] = [];
|
||||
const onError = (error: Error): void => { errors.push(error.message); };
|
||||
ctx.page.on('pageerror', onError);
|
||||
try {
|
||||
await openTerminal(ctx);
|
||||
await ctx.page.evaluate(`
|
||||
window.addon = new window.WebglAddon({ customGlyphs: true });
|
||||
window.term.loadAddon(window.addon);
|
||||
`);
|
||||
await ctx.page.evaluate(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
const auxiliaryDocument = frame.contentDocument;
|
||||
if (!auxiliaryDocument?.body || !window.term.element) {
|
||||
throw new Error('Auxiliary document and terminal element must be available');
|
||||
}
|
||||
const renderer = window.term._core?._renderService?._renderer?.value as ITestRendererWithAtlasCanvas | undefined;
|
||||
const atlasCanvas = renderer?._charAtlas?._tmpCanvas;
|
||||
if (!atlasCanvas) {
|
||||
throw new Error('Texture atlas canvas must be available');
|
||||
}
|
||||
// Model the atlas state after an uncached glyph has attached the measurement canvas.
|
||||
window.term.element.appendChild(atlasCanvas);
|
||||
auxiliaryDocument.body.appendChild(window.term.element);
|
||||
if (atlasCanvas.ownerDocument !== auxiliaryDocument) {
|
||||
throw new Error('Texture atlas canvas must be adopted into the auxiliary document');
|
||||
}
|
||||
auxiliaryDocument.createElement = () => {
|
||||
throw new Error('Not allowed to create elements in the auxiliary document');
|
||||
};
|
||||
});
|
||||
|
||||
await writeAndWaitForRender(ctx, '\u2591');
|
||||
await writeAndWaitForRender(ctx, 'X');
|
||||
|
||||
const line = await ctx.page.evaluate(() => window.term.buffer.active.getLine(0)?.translateToString(true));
|
||||
expect(errors, `renderer must not create pattern canvases in the auxiliary document: ${errors[0] ?? ''}`).toEqual([]);
|
||||
expect(line).toBe('\u2591X');
|
||||
} finally {
|
||||
ctx.page.off('pageerror', onError);
|
||||
await ctx.page.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user