Make terminal rendering work in popout windows
Add support for the parent element (what is passed to Terminal.open) being in a different (same origin) window. Accesses to DOM APIs such as requestAnimationFrame and devicePixelRatio thus need to be scoped to the corrent window, instead of assuming that it's the same as the window/global scope where the code is running. This is done by inferring a parent window at the creation time, and then storing it in CoreBrowserService, which is already passed to most places that need it. To catch future regressions, an ESLint rule that checks for global accesses is added (it uses AST selectors via the no-restricted-syntax rule). This should also be applicable when the parent element is an iframe. Fixes #3758
This commit is contained in:
@@ -9,12 +9,17 @@ export class CoreBrowserService implements ICoreBrowserService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
private _textarea: HTMLTextAreaElement
|
||||
private _textarea: HTMLTextAreaElement,
|
||||
public readonly window: Window & typeof globalThis
|
||||
) {
|
||||
}
|
||||
|
||||
public get dpr(): number {
|
||||
return this.window.devicePixelRatio;
|
||||
}
|
||||
|
||||
public get isFocused(): boolean {
|
||||
const docOrShadowRoot = this._textarea.getRootNode ? this._textarea.getRootNode() as Document | ShadowRoot : document;
|
||||
return docOrShadowRoot.activeElement === this._textarea && document.hasFocus();
|
||||
const docOrShadowRoot = this._textarea.getRootNode ? this._textarea.getRootNode() as Document | ShadowRoot : this._textarea.ownerDocument;
|
||||
return docOrShadowRoot.activeElement === this._textarea && this._textarea.ownerDocument.hasFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharSizeService, IRenderService } from 'browser/services/Services';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
interface ISelectionState {
|
||||
start: [number, number] | undefined;
|
||||
@@ -55,16 +55,17 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IDecorationService decorationService: IDecorationService,
|
||||
@IBufferService bufferService: IBufferService
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@ICoreBrowserService coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.register({ dispose: () => this._renderer.dispose() });
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end));
|
||||
this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
|
||||
this.register(this._renderDebouncer);
|
||||
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this._screenDprMonitor = new ScreenDprMonitor(coreBrowserService.window);
|
||||
this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange());
|
||||
this.register(this._screenDprMonitor);
|
||||
|
||||
@@ -84,12 +85,12 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
// dprchange should handle this case, we need this as well for browsers that don't support the
|
||||
// matchMedia query.
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange()));
|
||||
this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.onDevicePixelRatioChange()));
|
||||
|
||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
if ('IntersectionObserver' in coreBrowserService.window) {
|
||||
const observer = new coreBrowserService.window.IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
observer.observe(screenElement);
|
||||
this.register({ dispose: () => observer.disconnect() });
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IBufferLine } from 'common/Types';
|
||||
import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { MockMouseService, MockRenderService } from 'browser/TestUtils.test';
|
||||
import { MockCoreBrowserService, MockMouseService, MockRenderService } from 'browser/TestUtils.test';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
@@ -21,7 +21,7 @@ class TestSelectionService extends SelectionService {
|
||||
optionsService: IOptionsService,
|
||||
renderService: IRenderService
|
||||
) {
|
||||
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
|
||||
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService, new MockCoreBrowserService());
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as Browser from 'common/Platform';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
|
||||
import { IMouseService, ISelectionService, IRenderService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IBufferRange, ILinkifier2 } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
@@ -128,7 +128,8 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -270,7 +271,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
public refresh(isLinuxMouseSelection?: boolean): void {
|
||||
// Queue the refresh for the renderer
|
||||
if (!this._refreshAnimationFrame) {
|
||||
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
||||
this._refreshAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._refresh());
|
||||
}
|
||||
|
||||
// If the platform is Linux and the refresh call comes from a mouse event,
|
||||
@@ -406,7 +407,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = getCoordsRelativeToElement(window, event, this._screenElement)[1];
|
||||
let offset = getCoordsRelativeToElement(this._coreBrowserService.window, event, this._screenElement)[1];
|
||||
const terminalHeight = this._renderService.dimensions.canvasHeight;
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
@@ -491,7 +492,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
||||
}
|
||||
this._dragScrollIntervalTimer = window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
||||
this._dragScrollIntervalTimer = this._coreBrowserService.window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -502,7 +503,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._screenElement.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._screenElement.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
}
|
||||
clearInterval(this._dragScrollIntervalTimer);
|
||||
this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer);
|
||||
this._dragScrollIntervalTimer = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,16 @@ export interface ICoreBrowserService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
readonly isFocused: boolean;
|
||||
/**
|
||||
* Parent window that the terminal is rendered into. DOM and rendering APIs
|
||||
* (e.g. requestAnimationFrame) should be invoked in the context of this
|
||||
* window.
|
||||
*/
|
||||
readonly window: Window & typeof globalThis;
|
||||
/**
|
||||
* Helper for getting the devicePixelRatio of the parent window.
|
||||
*/
|
||||
readonly dpr: number;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
|
||||
Reference in New Issue
Block a user