Implement and default to text metrics measure strategy

Fixes #3449
This commit is contained in:
Daniel Imms
2023-12-29 03:40:44 -08:00
parent 6e351dd4ac
commit 559026bbf3
+45 -18
View File
@@ -8,12 +8,6 @@ import { EventEmitter } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
const enum MeasureSettings {
REPEAT = 32
}
export class CharSizeService extends Disposable implements ICharSizeService {
public serviceBrand: undefined;
@@ -32,7 +26,11 @@ export class CharSizeService extends Disposable implements ICharSizeService {
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
try {
this._measureStrategy = new TextMetricsMeasureStrategy(this._optionsService);
} catch {
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
}
this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
}
@@ -47,12 +45,7 @@ export class CharSizeService extends Disposable implements ICharSizeService {
}
interface IMeasureStrategy {
measure(): IReadonlyMeasureResult;
}
interface IReadonlyMeasureResult {
readonly width: number;
readonly height: number;
measure(): Readonly<IMeasureResult>;
}
interface IMeasureResult {
@@ -60,8 +53,10 @@ interface IMeasureResult {
height: number;
}
// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses
// ctx.measureText
const enum DomMeasureStrategyConstants {
REPEAT = 32
}
class DomMeasureStrategy implements IMeasureStrategy {
private _result: IMeasureResult = { width: 0, height: 0 };
private _measureElement: HTMLElement;
@@ -73,14 +68,14 @@ class DomMeasureStrategy implements IMeasureStrategy {
) {
this._measureElement = this._document.createElement('span');
this._measureElement.classList.add('xterm-char-measure-element');
this._measureElement.textContent = 'W'.repeat(MeasureSettings.REPEAT);
this._measureElement.textContent = 'W'.repeat(DomMeasureStrategyConstants.REPEAT);
this._measureElement.setAttribute('aria-hidden', 'true');
this._measureElement.style.whiteSpace = 'pre';
this._measureElement.style.fontKerning = 'none';
this._parentElement.appendChild(this._measureElement);
}
public measure(): IReadonlyMeasureResult {
public measure(): Readonly<IMeasureResult> {
this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily;
this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`;
@@ -93,10 +88,42 @@ class DomMeasureStrategy implements IMeasureStrategy {
// If values are 0 then the element is likely currently display:none, in which case we should
// retain the previous value.
if (geometry.width !== 0 && geometry.height !== 0) {
this._result.width = geometry.width / MeasureSettings.REPEAT;
this._result.width = geometry.width / DomMeasureStrategyConstants.REPEAT;
this._result.height = Math.ceil(geometry.height);
}
return this._result;
}
}
class TextMetricsMeasureStrategy implements IMeasureStrategy {
private _result: IMeasureResult = { width: 0, height: 0 };
private _canvas: OffscreenCanvas;
private _ctx: OffscreenCanvasRenderingContext2D;
constructor(
private _optionsService: IOptionsService
) {
// This will throw if any required API is not supported
this._canvas = new OffscreenCanvas(100, 100);
this._ctx = this._canvas.getContext('2d')!;
const a = this._ctx.measureText('W');
if (!('width' in a && 'fontBoundingBoxAscent' in a && 'fontBoundingBoxDescent' in a)) {
throw new Error('Required font metrics not supported');
}
}
public measure(): Readonly<IMeasureResult> {
this._ctx.font = `${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;
const metrics = this._ctx.measureText('W');
// Sanity check that the values are not 0
if (metrics.width !== 0 && metrics.fontBoundingBoxAscent !== 0) {
this._result.width = metrics.width;
this._result.height = Math.ceil(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent);
}
return this._result;
}
}