/** * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { ICoreBrowserService } from './Services'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { addDisposableDomListener } from 'browser/Lifecycle'; export class CoreBrowserService extends Disposable implements ICoreBrowserService { public serviceBrand: undefined; private _isFocused = false; private _cachedIsFocused: boolean | undefined = undefined; private _screenDprMonitor = new ScreenDprMonitor(this._window); private readonly _onDprChange = this.register(new EventEmitter()); public readonly onDprChange = this._onDprChange.event; private readonly _onWindowChange = this.register(new EventEmitter()); public readonly onWindowChange = this._onWindowChange.event; constructor( private _textarea: HTMLTextAreaElement, private _window: Window & typeof globalThis, public readonly mainDocument: Document ) { super(); // Monitor device pixel ratio this.register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w))); this.register(forwardEvent(this._screenDprMonitor.onDprChange, this._onDprChange)); this._textarea.addEventListener('focus', () => this._isFocused = true); this._textarea.addEventListener('blur', () => this._isFocused = false); } public get window(): Window & typeof globalThis { return this._window; } public set window(value: Window & typeof globalThis) { if (this._window !== value) { this._window = value; this._onWindowChange.fire(this._window); } } public get dpr(): number { return this.window.devicePixelRatio; } public get isFocused(): boolean { if (this._cachedIsFocused === undefined) { this._cachedIsFocused = this._isFocused && this._textarea.ownerDocument.hasFocus(); queueMicrotask(() => this._cachedIsFocused = undefined); } return this._cachedIsFocused; } } /** * The screen device pixel ratio monitor allows listening for when the * window.devicePixelRatio value changes. This is done not with polling but with * the use of window.matchMedia to watch media queries. When the event fires, * the listener will be reattached using a different media query to ensure that * any further changes will register. * * The listener should fire on both window zoom changes and switching to a * monitor with a different DPI. */ class ScreenDprMonitor extends Disposable { private _currentDevicePixelRatio: number; private _outerListener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | undefined; private _resolutionMediaMatchList: MediaQueryList | undefined; private _windowResizeListener = this.register(new MutableDisposable()); private readonly _onDprChange = this.register(new EventEmitter()); public readonly onDprChange = this._onDprChange.event; constructor(private _parentWindow: Window) { super(); // Initialize listener and dpr value this._outerListener = () => this._setDprAndFireIfDiffers(); this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; this._updateDpr(); // Monitor active window resize this._setWindowResizeListener(); // Setup additional disposables this.register(toDisposable(() => this.clearListener())); } public setWindow(parentWindow: Window): void { this._parentWindow = parentWindow; this._setWindowResizeListener(); this._setDprAndFireIfDiffers(); } private _setWindowResizeListener(): void { this._windowResizeListener.value = addDisposableDomListener(this._parentWindow, 'resize', () => this._setDprAndFireIfDiffers()); } private _setDprAndFireIfDiffers(): void { if (this._parentWindow.devicePixelRatio !== this._currentDevicePixelRatio) { this._onDprChange.fire(this._parentWindow.devicePixelRatio); } this._updateDpr(); } private _updateDpr(): void { if (!this._outerListener) { return; } // Clear listeners for old DPR this._resolutionMediaMatchList?.removeListener(this._outerListener); // Add listeners for new DPR this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; this._resolutionMediaMatchList = this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`); this._resolutionMediaMatchList.addListener(this._outerListener); } public clearListener(): void { if (!this._resolutionMediaMatchList || !this._outerListener) { return; } this._resolutionMediaMatchList.removeListener(this._outerListener); this._resolutionMediaMatchList = undefined; this._outerListener = undefined; } }