Use OffscreenCanvas over DOM for WidthCache

Fixes #5548
This commit is contained in:
Daniel Imms
2025-12-30 02:57:09 -08:00
parent 00ad3eac40
commit ef978b370b
2 changed files with 39 additions and 59 deletions
-9
View File
@@ -112,15 +112,6 @@
top: 0; top: 0;
} }
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events { .xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default; cursor: default;
+39 -50
View File
@@ -3,6 +3,7 @@
* @license MIT * @license MIT
*/ */
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IDisposable } from 'common/Types'; import { IDisposable } from 'common/Types';
import { FontWeight } from 'common/services/Services'; import { FontWeight } from 'common/services/Services';
@@ -42,50 +43,22 @@ export class WidthCache implements IDisposable {
private _fontSize = 0; private _fontSize = 0;
private _weight: FontWeight = 'normal'; private _weight: FontWeight = 'normal';
private _weightBold: FontWeight = 'bold'; private _weightBold: FontWeight = 'bold';
private _container: HTMLDivElement; private _canvasElements: WidthCacheFontVariantCanvas[] = [];
private _measureElements: HTMLSpanElement[] = [];
constructor(_document: Document, _helperContainer: HTMLElement) { constructor(_document: Document, _helperContainer: HTMLElement) {
this._container = _document.createElement('div'); this._canvasElements = [
this._container.classList.add('xterm-width-cache-measure-container'); new WidthCacheFontVariantCanvas(),
this._container.setAttribute('aria-hidden', 'true'); new WidthCacheFontVariantCanvas(),
// SP should stack in spans new WidthCacheFontVariantCanvas(),
this._container.style.whiteSpace = 'pre'; new WidthCacheFontVariantCanvas()
// avoid undercuts in non-monospace fonts from kerning ];
this._container.style.fontKerning = 'none';
const regular = _document.createElement('span');
regular.classList.add('xterm-char-measure-element');
const bold = _document.createElement('span');
bold.classList.add('xterm-char-measure-element');
bold.style.fontWeight = 'bold';
const italic = _document.createElement('span');
italic.classList.add('xterm-char-measure-element');
italic.style.fontStyle = 'italic';
const boldItalic = _document.createElement('span');
boldItalic.classList.add('xterm-char-measure-element');
boldItalic.style.fontWeight = 'bold';
boldItalic.style.fontStyle = 'italic';
// NOTE: must be in order of FontVariant
this._measureElements = [regular, bold, italic, boldItalic];
this._container.appendChild(regular);
this._container.appendChild(bold);
this._container.appendChild(italic);
this._container.appendChild(boldItalic);
_helperContainer.appendChild(this._container);
this.clear(); this.clear();
} }
public dispose(): void { public dispose(): void {
this._container.remove(); // remove elements from DOM this._canvasElements.length = 0;
this._measureElements.length = 0; // release element refs this._holey = undefined; // free cache memory via GC
this._holey = undefined; // free cache memory via GC
} }
/** /**
@@ -104,10 +77,11 @@ export class WidthCache implements IDisposable {
*/ */
public setFont(font: string, fontSize: number, weight: FontWeight, weightBold: FontWeight): void { public setFont(font: string, fontSize: number, weight: FontWeight, weightBold: FontWeight): void {
// skip if nothing changed // skip if nothing changed
if (font === this._font if (
&& fontSize === this._fontSize font === this._font &&
&& weight === this._weight fontSize === this._fontSize &&
&& weightBold === this._weightBold weight === this._weight &&
weightBold === this._weightBold
) { ) {
return; return;
} }
@@ -117,12 +91,10 @@ export class WidthCache implements IDisposable {
this._weight = weight; this._weight = weight;
this._weightBold = weightBold; this._weightBold = weightBold;
this._container.style.fontFamily = this._font; this._canvasElements[FontVariant.REGULAR].setFont(font, fontSize, weight, false);
this._container.style.fontSize = `${this._fontSize}px`; this._canvasElements[FontVariant.BOLD].setFont(font, fontSize, weightBold, false);
this._measureElements[FontVariant.REGULAR].style.fontWeight = `${weight}`; this._canvasElements[FontVariant.ITALIC].setFont(font, fontSize, weight, true);
this._measureElements[FontVariant.BOLD].style.fontWeight = `${weightBold}`; this._canvasElements[FontVariant.BOLD_ITALIC].setFont(font, fontSize, weightBold, true);
this._measureElements[FontVariant.ITALIC].style.fontWeight = `${weight}`;
this._measureElements[FontVariant.BOLD_ITALIC].style.fontWeight = `${weightBold}`;
this.clear(); this.clear();
} }
@@ -160,8 +132,25 @@ export class WidthCache implements IDisposable {
} }
protected _measure(c: string, variant: FontVariant): number { protected _measure(c: string, variant: FontVariant): number {
const el = this._measureElements[variant]; return this._canvasElements[variant].measure(c);
el.textContent = c.repeat(WidthCacheSettings.REPEAT); }
return el.offsetWidth / WidthCacheSettings.REPEAT; }
class WidthCacheFontVariantCanvas {
private _canvas: OffscreenCanvas;
private _ctx: OffscreenCanvasRenderingContext2D;
constructor() {
this._canvas = new OffscreenCanvas(1, 1);
this._ctx = throwIfFalsy(this._canvas.getContext('2d'));
}
public setFont(fontFamily: string, fontSize: number, fontWeight: FontWeight, italic: boolean): void {
const fontStyle = italic ? 'italic' : '';
this._ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`.trim();
}
public measure(c: string): number {
return this._ctx.measureText(c).width;
} }
} }