From 559026bbf3ce2a74cbb7682663596895219c79d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 29 Dec 2023 03:40:44 -0800 Subject: [PATCH] Implement and default to text metrics measure strategy Fixes #3449 --- src/browser/services/CharSizeService.ts | 63 ++++++++++++++++++------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 614b9b30..ef0b1ab4 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -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; } 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 { 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 { + 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; + } +}