Path: blob/main/src/vs/editor/browser/config/fontMeasurements.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getWindowId } from '../../../base/browser/dom.js';6import { PixelRatio } from '../../../base/browser/pixelRatio.js';7import { Emitter } from '../../../base/common/event.js';8import { Disposable } from '../../../base/common/lifecycle.js';9import { CharWidthRequest, CharWidthRequestType, readCharWidths } from './charWidthReader.js';10import { EditorFontLigatures } from '../../common/config/editorOptions.js';11import { BareFontInfo, FontInfo, SERIALIZED_FONT_INFO_VERSION } from '../../common/config/fontInfo.js';1213/**14* Serializable font information.15*/16export interface ISerializedFontInfo {17readonly version: number;18readonly pixelRatio: number;19readonly fontFamily: string;20readonly fontWeight: string;21readonly fontSize: number;22readonly fontFeatureSettings: string;23readonly fontVariationSettings: string;24readonly lineHeight: number;25readonly letterSpacing: number;26readonly isMonospace: boolean;27readonly typicalHalfwidthCharacterWidth: number;28readonly typicalFullwidthCharacterWidth: number;29readonly canUseHalfwidthRightwardsArrow: boolean;30readonly spaceWidth: number;31readonly middotWidth: number;32readonly wsmiddotWidth: number;33readonly maxDigitWidth: number;34}3536export class FontMeasurementsImpl extends Disposable {3738private readonly _cache = new Map<number, FontMeasurementsCache>();3940private _evictUntrustedReadingsTimeout = -1;4142private readonly _onDidChange = this._register(new Emitter<void>());43public readonly onDidChange = this._onDidChange.event;4445public override dispose(): void {46if (this._evictUntrustedReadingsTimeout !== -1) {47clearTimeout(this._evictUntrustedReadingsTimeout);48this._evictUntrustedReadingsTimeout = -1;49}50super.dispose();51}5253/**54* Clear all cached font information and trigger a change event.55*/56public clearAllFontInfos(): void {57this._cache.clear();58this._onDidChange.fire();59}6061private _ensureCache(targetWindow: Window): FontMeasurementsCache {62const windowId = getWindowId(targetWindow);63let cache = this._cache.get(windowId);64if (!cache) {65cache = new FontMeasurementsCache();66this._cache.set(windowId, cache);67}68return cache;69}7071private _writeToCache(targetWindow: Window, item: BareFontInfo, value: FontInfo): void {72const cache = this._ensureCache(targetWindow);73cache.put(item, value);7475if (!value.isTrusted && this._evictUntrustedReadingsTimeout === -1) {76// Try reading again after some time77this._evictUntrustedReadingsTimeout = targetWindow.setTimeout(() => {78this._evictUntrustedReadingsTimeout = -1;79this._evictUntrustedReadings(targetWindow);80}, 5000);81}82}8384private _evictUntrustedReadings(targetWindow: Window): void {85const cache = this._ensureCache(targetWindow);86const values = cache.getValues();87let somethingRemoved = false;88for (const item of values) {89if (!item.isTrusted) {90somethingRemoved = true;91cache.remove(item);92}93}94if (somethingRemoved) {95this._onDidChange.fire();96}97}9899/**100* Serialized currently cached font information.101*/102public serializeFontInfo(targetWindow: Window): ISerializedFontInfo[] {103// Only save trusted font info (that has been measured in this running instance)104const cache = this._ensureCache(targetWindow);105return cache.getValues().filter(item => item.isTrusted);106}107108/**109* Restore previously serialized font informations.110*/111public restoreFontInfo(targetWindow: Window, savedFontInfos: ISerializedFontInfo[]): void {112// Take all the saved font info and insert them in the cache without the trusted flag.113// The reason for this is that a font might have been installed on the OS in the meantime.114for (const savedFontInfo of savedFontInfos) {115if (savedFontInfo.version !== SERIALIZED_FONT_INFO_VERSION) {116// cannot use older version117continue;118}119const fontInfo = new FontInfo(savedFontInfo, false);120this._writeToCache(targetWindow, fontInfo, fontInfo);121}122}123124/**125* Read font information.126*/127public readFontInfo(targetWindow: Window, bareFontInfo: BareFontInfo): FontInfo {128const cache = this._ensureCache(targetWindow);129if (!cache.has(bareFontInfo)) {130let readConfig = this._actualReadFontInfo(targetWindow, bareFontInfo);131132if (readConfig.typicalHalfwidthCharacterWidth <= 2 || readConfig.typicalFullwidthCharacterWidth <= 2 || readConfig.spaceWidth <= 2 || readConfig.maxDigitWidth <= 2) {133// Hey, it's Bug 14341 ... we couldn't read134readConfig = new FontInfo({135pixelRatio: PixelRatio.getInstance(targetWindow).value,136fontFamily: readConfig.fontFamily,137fontWeight: readConfig.fontWeight,138fontSize: readConfig.fontSize,139fontFeatureSettings: readConfig.fontFeatureSettings,140fontVariationSettings: readConfig.fontVariationSettings,141lineHeight: readConfig.lineHeight,142letterSpacing: readConfig.letterSpacing,143isMonospace: readConfig.isMonospace,144typicalHalfwidthCharacterWidth: Math.max(readConfig.typicalHalfwidthCharacterWidth, 5),145typicalFullwidthCharacterWidth: Math.max(readConfig.typicalFullwidthCharacterWidth, 5),146canUseHalfwidthRightwardsArrow: readConfig.canUseHalfwidthRightwardsArrow,147spaceWidth: Math.max(readConfig.spaceWidth, 5),148middotWidth: Math.max(readConfig.middotWidth, 5),149wsmiddotWidth: Math.max(readConfig.wsmiddotWidth, 5),150maxDigitWidth: Math.max(readConfig.maxDigitWidth, 5),151}, false);152}153154this._writeToCache(targetWindow, bareFontInfo, readConfig);155}156return cache.get(bareFontInfo);157}158159private _createRequest(chr: string, type: CharWidthRequestType, all: CharWidthRequest[], monospace: CharWidthRequest[] | null): CharWidthRequest {160const result = new CharWidthRequest(chr, type);161all.push(result);162monospace?.push(result);163return result;164}165166private _actualReadFontInfo(targetWindow: Window, bareFontInfo: BareFontInfo): FontInfo {167const all: CharWidthRequest[] = [];168const monospace: CharWidthRequest[] = [];169170const typicalHalfwidthCharacter = this._createRequest('n', CharWidthRequestType.Regular, all, monospace);171const typicalFullwidthCharacter = this._createRequest('\uff4d', CharWidthRequestType.Regular, all, null);172const space = this._createRequest(' ', CharWidthRequestType.Regular, all, monospace);173const digit0 = this._createRequest('0', CharWidthRequestType.Regular, all, monospace);174const digit1 = this._createRequest('1', CharWidthRequestType.Regular, all, monospace);175const digit2 = this._createRequest('2', CharWidthRequestType.Regular, all, monospace);176const digit3 = this._createRequest('3', CharWidthRequestType.Regular, all, monospace);177const digit4 = this._createRequest('4', CharWidthRequestType.Regular, all, monospace);178const digit5 = this._createRequest('5', CharWidthRequestType.Regular, all, monospace);179const digit6 = this._createRequest('6', CharWidthRequestType.Regular, all, monospace);180const digit7 = this._createRequest('7', CharWidthRequestType.Regular, all, monospace);181const digit8 = this._createRequest('8', CharWidthRequestType.Regular, all, monospace);182const digit9 = this._createRequest('9', CharWidthRequestType.Regular, all, monospace);183184// monospace test: used for whitespace rendering185const rightwardsArrow = this._createRequest('→', CharWidthRequestType.Regular, all, monospace);186const halfwidthRightwardsArrow = this._createRequest('→', CharWidthRequestType.Regular, all, null);187188// U+00B7 - MIDDLE DOT189const middot = this._createRequest('·', CharWidthRequestType.Regular, all, monospace);190191// U+2E31 - WORD SEPARATOR MIDDLE DOT192const wsmiddotWidth = this._createRequest(String.fromCharCode(0x2E31), CharWidthRequestType.Regular, all, null);193194// monospace test: some characters195const monospaceTestChars = '|/-_ilm%';196for (let i = 0, len = monospaceTestChars.length; i < len; i++) {197this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Regular, all, monospace);198this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Italic, all, monospace);199this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Bold, all, monospace);200}201202readCharWidths(targetWindow, bareFontInfo, all);203204const maxDigitWidth = Math.max(digit0.width, digit1.width, digit2.width, digit3.width, digit4.width, digit5.width, digit6.width, digit7.width, digit8.width, digit9.width);205206let isMonospace = (bareFontInfo.fontFeatureSettings === EditorFontLigatures.OFF);207const referenceWidth = monospace[0].width;208for (let i = 1, len = monospace.length; isMonospace && i < len; i++) {209const diff = referenceWidth - monospace[i].width;210if (diff < -0.001 || diff > 0.001) {211isMonospace = false;212break;213}214}215216let canUseHalfwidthRightwardsArrow = true;217if (isMonospace && halfwidthRightwardsArrow.width !== referenceWidth) {218// using a halfwidth rightwards arrow would break monospace...219canUseHalfwidthRightwardsArrow = false;220}221if (halfwidthRightwardsArrow.width > rightwardsArrow.width) {222// using a halfwidth rightwards arrow would paint a larger arrow than a regular rightwards arrow223canUseHalfwidthRightwardsArrow = false;224}225226return new FontInfo({227pixelRatio: PixelRatio.getInstance(targetWindow).value,228fontFamily: bareFontInfo.fontFamily,229fontWeight: bareFontInfo.fontWeight,230fontSize: bareFontInfo.fontSize,231fontFeatureSettings: bareFontInfo.fontFeatureSettings,232fontVariationSettings: bareFontInfo.fontVariationSettings,233lineHeight: bareFontInfo.lineHeight,234letterSpacing: bareFontInfo.letterSpacing,235isMonospace: isMonospace,236typicalHalfwidthCharacterWidth: typicalHalfwidthCharacter.width,237typicalFullwidthCharacterWidth: typicalFullwidthCharacter.width,238canUseHalfwidthRightwardsArrow: canUseHalfwidthRightwardsArrow,239spaceWidth: space.width,240middotWidth: middot.width,241wsmiddotWidth: wsmiddotWidth.width,242maxDigitWidth: maxDigitWidth243}, true);244}245}246247class FontMeasurementsCache {248249private readonly _keys: { [key: string]: BareFontInfo };250private readonly _values: { [key: string]: FontInfo };251252constructor() {253this._keys = Object.create(null);254this._values = Object.create(null);255}256257public has(item: BareFontInfo): boolean {258const itemId = item.getId();259return !!this._values[itemId];260}261262public get(item: BareFontInfo): FontInfo {263const itemId = item.getId();264return this._values[itemId];265}266267public put(item: BareFontInfo, value: FontInfo): void {268const itemId = item.getId();269this._keys[itemId] = item;270this._values[itemId] = value;271}272273public remove(item: BareFontInfo): void {274const itemId = item.getId();275delete this._keys[itemId];276delete this._values[itemId];277}278279public getValues(): FontInfo[] {280return Object.keys(this._keys).map(id => this._values[id]);281}282}283284export const FontMeasurements = new FontMeasurementsImpl();285286287