Skip to content

Commit 2027f70

Browse files
authored
aux window - make FontMeasurements and DevicePixelRatio per-window aware (microsoft#195888) (microsoft#203671)
1 parent 1a37acb commit 2027f70

25 files changed

+271
-203
lines changed

src/vs/base/browser/browser.ts

Lines changed: 2 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { $window, CodeWindow, mainWindow } from 'vs/base/browser/window';
7-
import { Emitter, Event } from 'vs/base/common/event';
8-
import { Disposable, markAsSingleton } from 'vs/base/common/lifecycle';
6+
import { CodeWindow, mainWindow } from 'vs/base/browser/window';
7+
import { Emitter } from 'vs/base/common/event';
98

109
class WindowManager {
1110

@@ -67,113 +66,13 @@ class WindowManager {
6766
}
6867
}
6968

70-
/**
71-
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
72-
*/
73-
class DevicePixelRatioMonitor extends Disposable {
74-
75-
private readonly _onDidChange = this._register(new Emitter<void>());
76-
readonly onDidChange = this._onDidChange.event;
77-
78-
private readonly _listener: () => void;
79-
private _mediaQueryList: MediaQueryList | null;
80-
81-
constructor() {
82-
super();
83-
84-
this._listener = () => this._handleChange(true);
85-
this._mediaQueryList = null;
86-
this._handleChange(false);
87-
}
88-
89-
private _handleChange(fireEvent: boolean): void {
90-
this._mediaQueryList?.removeEventListener('change', this._listener);
91-
92-
this._mediaQueryList = $window.matchMedia(`(resolution: ${$window.devicePixelRatio}dppx)`);
93-
this._mediaQueryList.addEventListener('change', this._listener);
94-
95-
if (fireEvent) {
96-
this._onDidChange.fire();
97-
}
98-
}
99-
}
100-
101-
class PixelRatioImpl extends Disposable {
102-
103-
private readonly _onDidChange = this._register(new Emitter<number>());
104-
readonly onDidChange = this._onDidChange.event;
105-
106-
private _value: number;
107-
108-
get value(): number {
109-
return this._value;
110-
}
111-
112-
constructor() {
113-
super();
114-
115-
this._value = this._getPixelRatio();
116-
117-
const dprMonitor = this._register(new DevicePixelRatioMonitor());
118-
this._register(dprMonitor.onDidChange(() => {
119-
this._value = this._getPixelRatio();
120-
this._onDidChange.fire(this._value);
121-
}));
122-
}
123-
124-
private _getPixelRatio(): number {
125-
const ctx: any = document.createElement('canvas').getContext('2d');
126-
const dpr = $window.devicePixelRatio || 1;
127-
const bsr = ctx.webkitBackingStorePixelRatio ||
128-
ctx.mozBackingStorePixelRatio ||
129-
ctx.msBackingStorePixelRatio ||
130-
ctx.oBackingStorePixelRatio ||
131-
ctx.backingStorePixelRatio || 1;
132-
return dpr / bsr;
133-
}
134-
}
135-
136-
class PixelRatioFacade {
137-
138-
private _pixelRatioMonitor: PixelRatioImpl | null = null;
139-
private _getOrCreatePixelRatioMonitor(): PixelRatioImpl {
140-
if (!this._pixelRatioMonitor) {
141-
this._pixelRatioMonitor = markAsSingleton(new PixelRatioImpl());
142-
}
143-
return this._pixelRatioMonitor;
144-
}
145-
146-
/**
147-
* Get the current value.
148-
*/
149-
get value(): number {
150-
return this._getOrCreatePixelRatioMonitor().value;
151-
}
152-
153-
/**
154-
* Listen for changes.
155-
*/
156-
get onDidChange(): Event<number> {
157-
return this._getOrCreatePixelRatioMonitor().onDidChange;
158-
}
159-
}
160-
16169
export function addMatchMediaChangeListener(targetWindow: Window, query: string | MediaQueryList, callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
16270
if (typeof query === 'string') {
16371
query = targetWindow.matchMedia(query);
16472
}
16573
query.addEventListener('change', callback);
16674
}
16775

168-
/**
169-
* Returns the pixel ratio.
170-
*
171-
* This is useful for rendering <canvas> elements at native screen resolution or for being used as
172-
* a cache key when storing font measurements. Fonts might render differently depending on resolution
173-
* and any measurements need to be discarded for example when a window is moved from a monitor to another.
174-
*/
175-
export const PixelRatio = new PixelRatioFacade();
176-
17776
/** A zoom index, e.g. 1, 2, 3 */
17877
export function setZoomLevel(zoomLevel: number, targetWindow: Window): void {
17978
WindowManager.INSTANCE.setZoomLevel(zoomLevel, targetWindow);

src/vs/base/browser/dom.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,21 @@ export const {
4242
const windows = new Map<number, IRegisteredCodeWindow>();
4343

4444
ensureCodeWindow(mainWindow, 1);
45-
windows.set(mainWindow.vscodeWindowId, { window: mainWindow, disposables: new DisposableStore() });
45+
const mainWindowRegistration = { window: mainWindow, disposables: new DisposableStore() };
46+
windows.set(mainWindow.vscodeWindowId, mainWindowRegistration);
4647

4748
const onDidRegisterWindow = new event.Emitter<IRegisteredCodeWindow>();
4849
const onDidUnregisterWindow = new event.Emitter<CodeWindow>();
4950
const onWillUnregisterWindow = new event.Emitter<CodeWindow>();
5051

52+
function getWindowById(windowId: number): IRegisteredCodeWindow | undefined;
53+
function getWindowById(windowId: number | undefined, fallbackToMain: true): IRegisteredCodeWindow;
54+
function getWindowById(windowId: number | undefined, fallbackToMain?: boolean): IRegisteredCodeWindow | undefined {
55+
const window = typeof windowId === 'number' ? windows.get(windowId) : undefined;
56+
57+
return window ?? (fallbackToMain ? mainWindowRegistration : undefined);
58+
}
59+
5160
return {
5261
onDidRegisterWindow: onDidRegisterWindow.event,
5362
onWillUnregisterWindow: onWillUnregisterWindow.event,
@@ -90,9 +99,7 @@ export const {
9099
hasWindow(windowId: number): boolean {
91100
return windows.has(windowId);
92101
},
93-
getWindowById(windowId: number): IRegisteredCodeWindow | undefined {
94-
return windows.get(windowId);
95-
},
102+
getWindowById,
96103
getWindow(e: Node | UIEvent | undefined | null): CodeWindow {
97104
const candidateNode = e as Node | undefined | null;
98105
if (candidateNode?.ownerDocument?.defaultView) {

src/vs/base/browser/pixelRatio.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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, onDidUnregisterWindow } from 'vs/base/browser/dom';
7+
import { Emitter, Event } from 'vs/base/common/event';
8+
import { Disposable, markAsSingleton } from 'vs/base/common/lifecycle';
9+
10+
/**
11+
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
12+
*/
13+
class DevicePixelRatioMonitor extends Disposable {
14+
15+
private readonly _onDidChange = this._register(new Emitter<void>());
16+
readonly onDidChange = this._onDidChange.event;
17+
18+
private readonly _listener: () => void;
19+
private _mediaQueryList: MediaQueryList | null;
20+
21+
constructor(targetWindow: Window) {
22+
super();
23+
24+
this._listener = () => this._handleChange(targetWindow, true);
25+
this._mediaQueryList = null;
26+
this._handleChange(targetWindow, false);
27+
}
28+
29+
private _handleChange(targetWindow: Window, fireEvent: boolean): void {
30+
this._mediaQueryList?.removeEventListener('change', this._listener);
31+
32+
this._mediaQueryList = targetWindow.matchMedia(`(resolution: ${targetWindow.devicePixelRatio}dppx)`);
33+
this._mediaQueryList.addEventListener('change', this._listener);
34+
35+
if (fireEvent) {
36+
this._onDidChange.fire();
37+
}
38+
}
39+
}
40+
41+
export interface IPixelRatioMonitor {
42+
readonly value: number;
43+
readonly onDidChange: Event<number>;
44+
}
45+
46+
class PixelRatioMonitorImpl extends Disposable implements IPixelRatioMonitor {
47+
48+
private readonly _onDidChange = this._register(new Emitter<number>());
49+
readonly onDidChange = this._onDidChange.event;
50+
51+
private _value: number;
52+
53+
get value(): number {
54+
return this._value;
55+
}
56+
57+
constructor(targetWindow: Window) {
58+
super();
59+
60+
this._value = this._getPixelRatio(targetWindow);
61+
62+
const dprMonitor = this._register(new DevicePixelRatioMonitor(targetWindow));
63+
this._register(dprMonitor.onDidChange(() => {
64+
this._value = this._getPixelRatio(targetWindow);
65+
this._onDidChange.fire(this._value);
66+
}));
67+
}
68+
69+
private _getPixelRatio(targetWindow: Window): number {
70+
const ctx: any = document.createElement('canvas').getContext('2d');
71+
const dpr = targetWindow.devicePixelRatio || 1;
72+
const bsr = ctx.webkitBackingStorePixelRatio ||
73+
ctx.mozBackingStorePixelRatio ||
74+
ctx.msBackingStorePixelRatio ||
75+
ctx.oBackingStorePixelRatio ||
76+
ctx.backingStorePixelRatio || 1;
77+
return dpr / bsr;
78+
}
79+
}
80+
81+
class PixelRatioMonitorFacade {
82+
83+
private readonly mapWindowIdToPixelRatioMonitor = new Map<number, PixelRatioMonitorImpl>();
84+
85+
private _getOrCreatePixelRatioMonitor(targetWindow: Window): PixelRatioMonitorImpl {
86+
const targetWindowId = getWindowId(targetWindow);
87+
let pixelRatioMonitor = this.mapWindowIdToPixelRatioMonitor.get(targetWindowId);
88+
if (!pixelRatioMonitor) {
89+
pixelRatioMonitor = markAsSingleton(new PixelRatioMonitorImpl(targetWindow));
90+
this.mapWindowIdToPixelRatioMonitor.set(targetWindowId, pixelRatioMonitor);
91+
92+
markAsSingleton(Event.once(onDidUnregisterWindow)(({ vscodeWindowId }) => {
93+
if (vscodeWindowId === targetWindowId) {
94+
pixelRatioMonitor?.dispose();
95+
this.mapWindowIdToPixelRatioMonitor.delete(targetWindowId);
96+
}
97+
}));
98+
}
99+
return pixelRatioMonitor;
100+
}
101+
102+
getInstance(targetWindow: Window): IPixelRatioMonitor {
103+
return this._getOrCreatePixelRatioMonitor(targetWindow);
104+
}
105+
}
106+
107+
/**
108+
* Returns the pixel ratio.
109+
*
110+
* This is useful for rendering <canvas> elements at native screen resolution or for being used as
111+
* a cache key when storing font measurements. Fonts might render differently depending on resolution
112+
* and any measurements need to be discarded for example when a window is moved from a monitor to another.
113+
*/
114+
export const PixelRatio = new PixelRatioMonitorFacade();

src/vs/base/test/browser/dom.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ suite('dom', () => {
346346
const windowId = getWindowId(mainWindow);
347347
assert.ok(typeof windowId === 'number');
348348
assert.strictEqual(getWindowById(windowId)?.window, mainWindow);
349+
assert.strictEqual(getWindowById(undefined, true).window, mainWindow);
349350
assert.strictEqual(hasWindow(windowId), true);
350351
assert.strictEqual(isAuxiliaryWindow(mainWindow), false);
351352
ensureCodeWindow(mainWindow, 1);

src/vs/editor/browser/config/charWidthReader.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { $window } from 'vs/base/browser/window';
76
import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo';
87
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
98

@@ -46,18 +45,18 @@ class DomCharWidthReader {
4645
this._testElements = null;
4746
}
4847

49-
public read(): void {
48+
public read(targetWindow: Window): void {
5049
// Create a test container with all these test elements
5150
this._createDomElements();
5251

5352
// Add the container to the DOM
54-
$window.document.body.appendChild(this._container!);
53+
targetWindow.document.body.appendChild(this._container!);
5554

5655
// Read character widths
5756
this._readFromDomElements();
5857

5958
// Remove the container from the DOM
60-
$window.document.body.removeChild(this._container!);
59+
targetWindow.document.body.removeChild(this._container!);
6160

6261
this._container = null;
6362
this._testElements = null;
@@ -138,7 +137,7 @@ class DomCharWidthReader {
138137
}
139138
}
140139

141-
export function readCharWidths(bareFontInfo: BareFontInfo, requests: CharWidthRequest[]): void {
140+
export function readCharWidths(targetWindow: Window, bareFontInfo: BareFontInfo, requests: CharWidthRequest[]): void {
142141
const reader = new DomCharWidthReader(bareFontInfo, requests);
143-
reader.read();
142+
reader.read(targetWindow);
144143
}

src/vs/editor/browser/config/editorConfiguration.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { BareFontInfo, FontInfo, IValidatedEditorOptions } from 'vs/editor/commo
1919
import { IDimension } from 'vs/editor/common/core/dimension';
2020
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
2121
import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
22+
import { getWindow, getWindowById } from 'vs/base/browser/dom';
23+
import { PixelRatio } from 'vs/base/browser/pixelRatio';
2224

2325
export interface IEditorConstructionOptions extends IEditorOptions {
2426
/**
@@ -48,6 +50,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
4850
private _lineNumbersDigitCount: number = 1;
4951
private _reservedHeight: number = 0;
5052
private _glyphMarginDecorationLaneCount: number = 1;
53+
private _targetWindowId: number;
5154

5255
private readonly _computeOptionsMemory: ComputeOptionsMemory = new ComputeOptionsMemory();
5356
/**
@@ -72,6 +75,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
7275
super();
7376
this.isSimpleWidget = isSimpleWidget;
7477
this._containerObserver = this._register(new ElementSizeObserver(container, options.dimension));
78+
this._targetWindowId = getWindow(container).vscodeWindowId;
7579

7680
this._rawOptions = deepCloneAndMigrateOptions(options);
7781
this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions);
@@ -85,7 +89,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
8589
this._register(TabFocus.onDidChangeTabFocus(() => this._recomputeOptions()));
8690
this._register(this._containerObserver.onDidChange(() => this._recomputeOptions()));
8791
this._register(FontMeasurements.onDidChange(() => this._recomputeOptions()));
88-
this._register(browser.PixelRatio.onDidChange(() => this._recomputeOptions()));
92+
this._register(PixelRatio.getInstance(getWindow(container)).onDidChange(() => this._recomputeOptions()));
8993
this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions()));
9094
}
9195

@@ -130,7 +134,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
130134
outerWidth: this._containerObserver.getWidth(),
131135
outerHeight: this._containerObserver.getHeight(),
132136
emptySelectionClipboard: browser.isWebKit || browser.isFirefox,
133-
pixelRatio: browser.PixelRatio.value,
137+
pixelRatio: PixelRatio.getInstance(getWindowById(this._targetWindowId, true).window).value,
134138
accessibilitySupport: (
135139
this._accessibilityService.isScreenReaderOptimized()
136140
? AccessibilitySupport.Enabled
@@ -140,7 +144,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
140144
}
141145

142146
protected _readFontInfo(bareFontInfo: BareFontInfo): FontInfo {
143-
return FontMeasurements.readFontInfo(bareFontInfo);
147+
return FontMeasurements.readFontInfo(getWindowById(this._targetWindowId, true).window, bareFontInfo);
144148
}
145149

146150
public getRawOptions(): IEditorOptions {

0 commit comments

Comments
 (0)