Skip to content

Commit

Permalink
aux window - make FontMeasurements and DevicePixelRatio per-windo…
Browse files Browse the repository at this point in the history
…w aware (#195888) (#203671)
  • Loading branch information
bpasero authored Jan 29, 2024
1 parent 1a37acb commit 2027f70
Show file tree
Hide file tree
Showing 25 changed files with 271 additions and 203 deletions.
105 changes: 2 additions & 103 deletions src/vs/base/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { $window, CodeWindow, mainWindow } from 'vs/base/browser/window';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, markAsSingleton } from 'vs/base/common/lifecycle';
import { CodeWindow, mainWindow } from 'vs/base/browser/window';
import { Emitter } from 'vs/base/common/event';

class WindowManager {

Expand Down Expand Up @@ -67,113 +66,13 @@ class WindowManager {
}
}

/**
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
*/
class DevicePixelRatioMonitor extends Disposable {

private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;

private readonly _listener: () => void;
private _mediaQueryList: MediaQueryList | null;

constructor() {
super();

this._listener = () => this._handleChange(true);
this._mediaQueryList = null;
this._handleChange(false);
}

private _handleChange(fireEvent: boolean): void {
this._mediaQueryList?.removeEventListener('change', this._listener);

this._mediaQueryList = $window.matchMedia(`(resolution: ${$window.devicePixelRatio}dppx)`);
this._mediaQueryList.addEventListener('change', this._listener);

if (fireEvent) {
this._onDidChange.fire();
}
}
}

class PixelRatioImpl extends Disposable {

private readonly _onDidChange = this._register(new Emitter<number>());
readonly onDidChange = this._onDidChange.event;

private _value: number;

get value(): number {
return this._value;
}

constructor() {
super();

this._value = this._getPixelRatio();

const dprMonitor = this._register(new DevicePixelRatioMonitor());
this._register(dprMonitor.onDidChange(() => {
this._value = this._getPixelRatio();
this._onDidChange.fire(this._value);
}));
}

private _getPixelRatio(): number {
const ctx: any = document.createElement('canvas').getContext('2d');
const dpr = $window.devicePixelRatio || 1;
const bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
}

class PixelRatioFacade {

private _pixelRatioMonitor: PixelRatioImpl | null = null;
private _getOrCreatePixelRatioMonitor(): PixelRatioImpl {
if (!this._pixelRatioMonitor) {
this._pixelRatioMonitor = markAsSingleton(new PixelRatioImpl());
}
return this._pixelRatioMonitor;
}

/**
* Get the current value.
*/
get value(): number {
return this._getOrCreatePixelRatioMonitor().value;
}

/**
* Listen for changes.
*/
get onDidChange(): Event<number> {
return this._getOrCreatePixelRatioMonitor().onDidChange;
}
}

export function addMatchMediaChangeListener(targetWindow: Window, query: string | MediaQueryList, callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
if (typeof query === 'string') {
query = targetWindow.matchMedia(query);
}
query.addEventListener('change', callback);
}

/**
* Returns the pixel ratio.
*
* This is useful for rendering <canvas> elements at native screen resolution or for being used as
* a cache key when storing font measurements. Fonts might render differently depending on resolution
* and any measurements need to be discarded for example when a window is moved from a monitor to another.
*/
export const PixelRatio = new PixelRatioFacade();

/** A zoom index, e.g. 1, 2, 3 */
export function setZoomLevel(zoomLevel: number, targetWindow: Window): void {
WindowManager.INSTANCE.setZoomLevel(zoomLevel, targetWindow);
Expand Down
15 changes: 11 additions & 4 deletions src/vs/base/browser/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,21 @@ export const {
const windows = new Map<number, IRegisteredCodeWindow>();

ensureCodeWindow(mainWindow, 1);
windows.set(mainWindow.vscodeWindowId, { window: mainWindow, disposables: new DisposableStore() });
const mainWindowRegistration = { window: mainWindow, disposables: new DisposableStore() };
windows.set(mainWindow.vscodeWindowId, mainWindowRegistration);

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

function getWindowById(windowId: number): IRegisteredCodeWindow | undefined;
function getWindowById(windowId: number | undefined, fallbackToMain: true): IRegisteredCodeWindow;
function getWindowById(windowId: number | undefined, fallbackToMain?: boolean): IRegisteredCodeWindow | undefined {
const window = typeof windowId === 'number' ? windows.get(windowId) : undefined;

return window ?? (fallbackToMain ? mainWindowRegistration : undefined);
}

return {
onDidRegisterWindow: onDidRegisterWindow.event,
onWillUnregisterWindow: onWillUnregisterWindow.event,
Expand Down Expand Up @@ -90,9 +99,7 @@ export const {
hasWindow(windowId: number): boolean {
return windows.has(windowId);
},
getWindowById(windowId: number): IRegisteredCodeWindow | undefined {
return windows.get(windowId);
},
getWindowById,
getWindow(e: Node | UIEvent | undefined | null): CodeWindow {
const candidateNode = e as Node | undefined | null;
if (candidateNode?.ownerDocument?.defaultView) {
Expand Down
114 changes: 114 additions & 0 deletions src/vs/base/browser/pixelRatio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { getWindowId, onDidUnregisterWindow } from 'vs/base/browser/dom';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, markAsSingleton } from 'vs/base/common/lifecycle';

/**
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
*/
class DevicePixelRatioMonitor extends Disposable {

private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;

private readonly _listener: () => void;
private _mediaQueryList: MediaQueryList | null;

constructor(targetWindow: Window) {
super();

this._listener = () => this._handleChange(targetWindow, true);
this._mediaQueryList = null;
this._handleChange(targetWindow, false);
}

private _handleChange(targetWindow: Window, fireEvent: boolean): void {
this._mediaQueryList?.removeEventListener('change', this._listener);

this._mediaQueryList = targetWindow.matchMedia(`(resolution: ${targetWindow.devicePixelRatio}dppx)`);
this._mediaQueryList.addEventListener('change', this._listener);

if (fireEvent) {
this._onDidChange.fire();
}
}
}

export interface IPixelRatioMonitor {
readonly value: number;
readonly onDidChange: Event<number>;
}

class PixelRatioMonitorImpl extends Disposable implements IPixelRatioMonitor {

private readonly _onDidChange = this._register(new Emitter<number>());
readonly onDidChange = this._onDidChange.event;

private _value: number;

get value(): number {
return this._value;
}

constructor(targetWindow: Window) {
super();

this._value = this._getPixelRatio(targetWindow);

const dprMonitor = this._register(new DevicePixelRatioMonitor(targetWindow));
this._register(dprMonitor.onDidChange(() => {
this._value = this._getPixelRatio(targetWindow);
this._onDidChange.fire(this._value);
}));
}

private _getPixelRatio(targetWindow: Window): number {
const ctx: any = document.createElement('canvas').getContext('2d');
const dpr = targetWindow.devicePixelRatio || 1;
const bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
}

class PixelRatioMonitorFacade {

private readonly mapWindowIdToPixelRatioMonitor = new Map<number, PixelRatioMonitorImpl>();

private _getOrCreatePixelRatioMonitor(targetWindow: Window): PixelRatioMonitorImpl {
const targetWindowId = getWindowId(targetWindow);
let pixelRatioMonitor = this.mapWindowIdToPixelRatioMonitor.get(targetWindowId);
if (!pixelRatioMonitor) {
pixelRatioMonitor = markAsSingleton(new PixelRatioMonitorImpl(targetWindow));
this.mapWindowIdToPixelRatioMonitor.set(targetWindowId, pixelRatioMonitor);

markAsSingleton(Event.once(onDidUnregisterWindow)(({ vscodeWindowId }) => {
if (vscodeWindowId === targetWindowId) {
pixelRatioMonitor?.dispose();
this.mapWindowIdToPixelRatioMonitor.delete(targetWindowId);
}
}));
}
return pixelRatioMonitor;
}

getInstance(targetWindow: Window): IPixelRatioMonitor {
return this._getOrCreatePixelRatioMonitor(targetWindow);
}
}

/**
* Returns the pixel ratio.
*
* This is useful for rendering <canvas> elements at native screen resolution or for being used as
* a cache key when storing font measurements. Fonts might render differently depending on resolution
* and any measurements need to be discarded for example when a window is moved from a monitor to another.
*/
export const PixelRatio = new PixelRatioMonitorFacade();
1 change: 1 addition & 0 deletions src/vs/base/test/browser/dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ suite('dom', () => {
const windowId = getWindowId(mainWindow);
assert.ok(typeof windowId === 'number');
assert.strictEqual(getWindowById(windowId)?.window, mainWindow);
assert.strictEqual(getWindowById(undefined, true).window, mainWindow);
assert.strictEqual(hasWindow(windowId), true);
assert.strictEqual(isAuxiliaryWindow(mainWindow), false);
ensureCodeWindow(mainWindow, 1);
Expand Down
11 changes: 5 additions & 6 deletions src/vs/editor/browser/config/charWidthReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { $window } from 'vs/base/browser/window';
import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';

Expand Down Expand Up @@ -46,18 +45,18 @@ class DomCharWidthReader {
this._testElements = null;
}

public read(): void {
public read(targetWindow: Window): void {
// Create a test container with all these test elements
this._createDomElements();

// Add the container to the DOM
$window.document.body.appendChild(this._container!);
targetWindow.document.body.appendChild(this._container!);

// Read character widths
this._readFromDomElements();

// Remove the container from the DOM
$window.document.body.removeChild(this._container!);
targetWindow.document.body.removeChild(this._container!);

this._container = null;
this._testElements = null;
Expand Down Expand Up @@ -138,7 +137,7 @@ class DomCharWidthReader {
}
}

export function readCharWidths(bareFontInfo: BareFontInfo, requests: CharWidthRequest[]): void {
export function readCharWidths(targetWindow: Window, bareFontInfo: BareFontInfo, requests: CharWidthRequest[]): void {
const reader = new DomCharWidthReader(bareFontInfo, requests);
reader.read();
reader.read(targetWindow);
}
10 changes: 7 additions & 3 deletions src/vs/editor/browser/config/editorConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { BareFontInfo, FontInfo, IValidatedEditorOptions } from 'vs/editor/commo
import { IDimension } from 'vs/editor/common/core/dimension';
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { getWindow, getWindowById } from 'vs/base/browser/dom';
import { PixelRatio } from 'vs/base/browser/pixelRatio';

export interface IEditorConstructionOptions extends IEditorOptions {
/**
Expand Down Expand Up @@ -48,6 +50,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
private _lineNumbersDigitCount: number = 1;
private _reservedHeight: number = 0;
private _glyphMarginDecorationLaneCount: number = 1;
private _targetWindowId: number;

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

this._rawOptions = deepCloneAndMigrateOptions(options);
this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions);
Expand All @@ -85,7 +89,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
this._register(TabFocus.onDidChangeTabFocus(() => this._recomputeOptions()));
this._register(this._containerObserver.onDidChange(() => this._recomputeOptions()));
this._register(FontMeasurements.onDidChange(() => this._recomputeOptions()));
this._register(browser.PixelRatio.onDidChange(() => this._recomputeOptions()));
this._register(PixelRatio.getInstance(getWindow(container)).onDidChange(() => this._recomputeOptions()));
this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions()));
}

Expand Down Expand Up @@ -130,7 +134,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
outerWidth: this._containerObserver.getWidth(),
outerHeight: this._containerObserver.getHeight(),
emptySelectionClipboard: browser.isWebKit || browser.isFirefox,
pixelRatio: browser.PixelRatio.value,
pixelRatio: PixelRatio.getInstance(getWindowById(this._targetWindowId, true).window).value,
accessibilitySupport: (
this._accessibilityService.isScreenReaderOptimized()
? AccessibilitySupport.Enabled
Expand All @@ -140,7 +144,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat
}

protected _readFontInfo(bareFontInfo: BareFontInfo): FontInfo {
return FontMeasurements.readFontInfo(bareFontInfo);
return FontMeasurements.readFontInfo(getWindowById(this._targetWindowId, true).window, bareFontInfo);
}

public getRawOptions(): IEditorOptions {
Expand Down
Loading

0 comments on commit 2027f70

Please sign in to comment.