Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/config/fontMeasurements.ts
3294 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { getWindowId } from '../../../base/browser/dom.js';
7
import { PixelRatio } from '../../../base/browser/pixelRatio.js';
8
import { Emitter } from '../../../base/common/event.js';
9
import { Disposable } from '../../../base/common/lifecycle.js';
10
import { CharWidthRequest, CharWidthRequestType, readCharWidths } from './charWidthReader.js';
11
import { EditorFontLigatures } from '../../common/config/editorOptions.js';
12
import { BareFontInfo, FontInfo, SERIALIZED_FONT_INFO_VERSION } from '../../common/config/fontInfo.js';
13
14
/**
15
* Serializable font information.
16
*/
17
export interface ISerializedFontInfo {
18
readonly version: number;
19
readonly pixelRatio: number;
20
readonly fontFamily: string;
21
readonly fontWeight: string;
22
readonly fontSize: number;
23
readonly fontFeatureSettings: string;
24
readonly fontVariationSettings: string;
25
readonly lineHeight: number;
26
readonly letterSpacing: number;
27
readonly isMonospace: boolean;
28
readonly typicalHalfwidthCharacterWidth: number;
29
readonly typicalFullwidthCharacterWidth: number;
30
readonly canUseHalfwidthRightwardsArrow: boolean;
31
readonly spaceWidth: number;
32
readonly middotWidth: number;
33
readonly wsmiddotWidth: number;
34
readonly maxDigitWidth: number;
35
}
36
37
export class FontMeasurementsImpl extends Disposable {
38
39
private readonly _cache = new Map<number, FontMeasurementsCache>();
40
41
private _evictUntrustedReadingsTimeout = -1;
42
43
private readonly _onDidChange = this._register(new Emitter<void>());
44
public readonly onDidChange = this._onDidChange.event;
45
46
public override dispose(): void {
47
if (this._evictUntrustedReadingsTimeout !== -1) {
48
clearTimeout(this._evictUntrustedReadingsTimeout);
49
this._evictUntrustedReadingsTimeout = -1;
50
}
51
super.dispose();
52
}
53
54
/**
55
* Clear all cached font information and trigger a change event.
56
*/
57
public clearAllFontInfos(): void {
58
this._cache.clear();
59
this._onDidChange.fire();
60
}
61
62
private _ensureCache(targetWindow: Window): FontMeasurementsCache {
63
const windowId = getWindowId(targetWindow);
64
let cache = this._cache.get(windowId);
65
if (!cache) {
66
cache = new FontMeasurementsCache();
67
this._cache.set(windowId, cache);
68
}
69
return cache;
70
}
71
72
private _writeToCache(targetWindow: Window, item: BareFontInfo, value: FontInfo): void {
73
const cache = this._ensureCache(targetWindow);
74
cache.put(item, value);
75
76
if (!value.isTrusted && this._evictUntrustedReadingsTimeout === -1) {
77
// Try reading again after some time
78
this._evictUntrustedReadingsTimeout = targetWindow.setTimeout(() => {
79
this._evictUntrustedReadingsTimeout = -1;
80
this._evictUntrustedReadings(targetWindow);
81
}, 5000);
82
}
83
}
84
85
private _evictUntrustedReadings(targetWindow: Window): void {
86
const cache = this._ensureCache(targetWindow);
87
const values = cache.getValues();
88
let somethingRemoved = false;
89
for (const item of values) {
90
if (!item.isTrusted) {
91
somethingRemoved = true;
92
cache.remove(item);
93
}
94
}
95
if (somethingRemoved) {
96
this._onDidChange.fire();
97
}
98
}
99
100
/**
101
* Serialized currently cached font information.
102
*/
103
public serializeFontInfo(targetWindow: Window): ISerializedFontInfo[] {
104
// Only save trusted font info (that has been measured in this running instance)
105
const cache = this._ensureCache(targetWindow);
106
return cache.getValues().filter(item => item.isTrusted);
107
}
108
109
/**
110
* Restore previously serialized font informations.
111
*/
112
public restoreFontInfo(targetWindow: Window, savedFontInfos: ISerializedFontInfo[]): void {
113
// Take all the saved font info and insert them in the cache without the trusted flag.
114
// The reason for this is that a font might have been installed on the OS in the meantime.
115
for (const savedFontInfo of savedFontInfos) {
116
if (savedFontInfo.version !== SERIALIZED_FONT_INFO_VERSION) {
117
// cannot use older version
118
continue;
119
}
120
const fontInfo = new FontInfo(savedFontInfo, false);
121
this._writeToCache(targetWindow, fontInfo, fontInfo);
122
}
123
}
124
125
/**
126
* Read font information.
127
*/
128
public readFontInfo(targetWindow: Window, bareFontInfo: BareFontInfo): FontInfo {
129
const cache = this._ensureCache(targetWindow);
130
if (!cache.has(bareFontInfo)) {
131
let readConfig = this._actualReadFontInfo(targetWindow, bareFontInfo);
132
133
if (readConfig.typicalHalfwidthCharacterWidth <= 2 || readConfig.typicalFullwidthCharacterWidth <= 2 || readConfig.spaceWidth <= 2 || readConfig.maxDigitWidth <= 2) {
134
// Hey, it's Bug 14341 ... we couldn't read
135
readConfig = new FontInfo({
136
pixelRatio: PixelRatio.getInstance(targetWindow).value,
137
fontFamily: readConfig.fontFamily,
138
fontWeight: readConfig.fontWeight,
139
fontSize: readConfig.fontSize,
140
fontFeatureSettings: readConfig.fontFeatureSettings,
141
fontVariationSettings: readConfig.fontVariationSettings,
142
lineHeight: readConfig.lineHeight,
143
letterSpacing: readConfig.letterSpacing,
144
isMonospace: readConfig.isMonospace,
145
typicalHalfwidthCharacterWidth: Math.max(readConfig.typicalHalfwidthCharacterWidth, 5),
146
typicalFullwidthCharacterWidth: Math.max(readConfig.typicalFullwidthCharacterWidth, 5),
147
canUseHalfwidthRightwardsArrow: readConfig.canUseHalfwidthRightwardsArrow,
148
spaceWidth: Math.max(readConfig.spaceWidth, 5),
149
middotWidth: Math.max(readConfig.middotWidth, 5),
150
wsmiddotWidth: Math.max(readConfig.wsmiddotWidth, 5),
151
maxDigitWidth: Math.max(readConfig.maxDigitWidth, 5),
152
}, false);
153
}
154
155
this._writeToCache(targetWindow, bareFontInfo, readConfig);
156
}
157
return cache.get(bareFontInfo);
158
}
159
160
private _createRequest(chr: string, type: CharWidthRequestType, all: CharWidthRequest[], monospace: CharWidthRequest[] | null): CharWidthRequest {
161
const result = new CharWidthRequest(chr, type);
162
all.push(result);
163
monospace?.push(result);
164
return result;
165
}
166
167
private _actualReadFontInfo(targetWindow: Window, bareFontInfo: BareFontInfo): FontInfo {
168
const all: CharWidthRequest[] = [];
169
const monospace: CharWidthRequest[] = [];
170
171
const typicalHalfwidthCharacter = this._createRequest('n', CharWidthRequestType.Regular, all, monospace);
172
const typicalFullwidthCharacter = this._createRequest('\uff4d', CharWidthRequestType.Regular, all, null);
173
const space = this._createRequest(' ', CharWidthRequestType.Regular, all, monospace);
174
const digit0 = this._createRequest('0', CharWidthRequestType.Regular, all, monospace);
175
const digit1 = this._createRequest('1', CharWidthRequestType.Regular, all, monospace);
176
const digit2 = this._createRequest('2', CharWidthRequestType.Regular, all, monospace);
177
const digit3 = this._createRequest('3', CharWidthRequestType.Regular, all, monospace);
178
const digit4 = this._createRequest('4', CharWidthRequestType.Regular, all, monospace);
179
const digit5 = this._createRequest('5', CharWidthRequestType.Regular, all, monospace);
180
const digit6 = this._createRequest('6', CharWidthRequestType.Regular, all, monospace);
181
const digit7 = this._createRequest('7', CharWidthRequestType.Regular, all, monospace);
182
const digit8 = this._createRequest('8', CharWidthRequestType.Regular, all, monospace);
183
const digit9 = this._createRequest('9', CharWidthRequestType.Regular, all, monospace);
184
185
// monospace test: used for whitespace rendering
186
const rightwardsArrow = this._createRequest('→', CharWidthRequestType.Regular, all, monospace);
187
const halfwidthRightwardsArrow = this._createRequest('→', CharWidthRequestType.Regular, all, null);
188
189
// U+00B7 - MIDDLE DOT
190
const middot = this._createRequest('·', CharWidthRequestType.Regular, all, monospace);
191
192
// U+2E31 - WORD SEPARATOR MIDDLE DOT
193
const wsmiddotWidth = this._createRequest(String.fromCharCode(0x2E31), CharWidthRequestType.Regular, all, null);
194
195
// monospace test: some characters
196
const monospaceTestChars = '|/-_ilm%';
197
for (let i = 0, len = monospaceTestChars.length; i < len; i++) {
198
this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Regular, all, monospace);
199
this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Italic, all, monospace);
200
this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Bold, all, monospace);
201
}
202
203
readCharWidths(targetWindow, bareFontInfo, all);
204
205
const maxDigitWidth = Math.max(digit0.width, digit1.width, digit2.width, digit3.width, digit4.width, digit5.width, digit6.width, digit7.width, digit8.width, digit9.width);
206
207
let isMonospace = (bareFontInfo.fontFeatureSettings === EditorFontLigatures.OFF);
208
const referenceWidth = monospace[0].width;
209
for (let i = 1, len = monospace.length; isMonospace && i < len; i++) {
210
const diff = referenceWidth - monospace[i].width;
211
if (diff < -0.001 || diff > 0.001) {
212
isMonospace = false;
213
break;
214
}
215
}
216
217
let canUseHalfwidthRightwardsArrow = true;
218
if (isMonospace && halfwidthRightwardsArrow.width !== referenceWidth) {
219
// using a halfwidth rightwards arrow would break monospace...
220
canUseHalfwidthRightwardsArrow = false;
221
}
222
if (halfwidthRightwardsArrow.width > rightwardsArrow.width) {
223
// using a halfwidth rightwards arrow would paint a larger arrow than a regular rightwards arrow
224
canUseHalfwidthRightwardsArrow = false;
225
}
226
227
return new FontInfo({
228
pixelRatio: PixelRatio.getInstance(targetWindow).value,
229
fontFamily: bareFontInfo.fontFamily,
230
fontWeight: bareFontInfo.fontWeight,
231
fontSize: bareFontInfo.fontSize,
232
fontFeatureSettings: bareFontInfo.fontFeatureSettings,
233
fontVariationSettings: bareFontInfo.fontVariationSettings,
234
lineHeight: bareFontInfo.lineHeight,
235
letterSpacing: bareFontInfo.letterSpacing,
236
isMonospace: isMonospace,
237
typicalHalfwidthCharacterWidth: typicalHalfwidthCharacter.width,
238
typicalFullwidthCharacterWidth: typicalFullwidthCharacter.width,
239
canUseHalfwidthRightwardsArrow: canUseHalfwidthRightwardsArrow,
240
spaceWidth: space.width,
241
middotWidth: middot.width,
242
wsmiddotWidth: wsmiddotWidth.width,
243
maxDigitWidth: maxDigitWidth
244
}, true);
245
}
246
}
247
248
class FontMeasurementsCache {
249
250
private readonly _keys: { [key: string]: BareFontInfo };
251
private readonly _values: { [key: string]: FontInfo };
252
253
constructor() {
254
this._keys = Object.create(null);
255
this._values = Object.create(null);
256
}
257
258
public has(item: BareFontInfo): boolean {
259
const itemId = item.getId();
260
return !!this._values[itemId];
261
}
262
263
public get(item: BareFontInfo): FontInfo {
264
const itemId = item.getId();
265
return this._values[itemId];
266
}
267
268
public put(item: BareFontInfo, value: FontInfo): void {
269
const itemId = item.getId();
270
this._keys[itemId] = item;
271
this._values[itemId] = value;
272
}
273
274
public remove(item: BareFontInfo): void {
275
const itemId = item.getId();
276
delete this._keys[itemId];
277
delete this._values[itemId];
278
}
279
280
public getValues(): FontInfo[] {
281
return Object.keys(this._keys).map(id => this._values[id]);
282
}
283
}
284
285
export const FontMeasurements = new FontMeasurementsImpl();
286
287