Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make xterm.js multi-window aware #4846

Merged
merged 8 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addons/xterm-addon-canvas/src/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
) {
super();
this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
this._canvas = document.createElement('canvas');
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
Expand Down
2 changes: 1 addition & 1 deletion addons/xterm-addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._updateCursorBlink();
this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));

this._canvas = document.createElement('canvas');
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');

const contextAttributes = {
antialias: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
protected readonly _themeService: IThemeService
) {
super();
this._canvas = document.createElement('canvas');
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
Expand Down
25 changes: 9 additions & 16 deletions src/browser/AccessibilityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import * as Strings from 'browser/LocalizableStrings';
import { ITerminal, IRenderDebouncer } from 'browser/Types';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { IRenderService } from 'browser/services/Services';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IBuffer } from 'common/buffer/Types';
import { IInstantiationService } from 'common/services/Services';

const MAX_ROWS_TO_READ = 20;

Expand All @@ -29,8 +28,6 @@ export class AccessibilityManager extends Disposable {
private _liveRegionLineCount: number = 0;
private _liveRegionDebouncer: IRenderDebouncer;

private _screenDprMonitor: ScreenDprMonitor;

private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;

Expand All @@ -49,13 +46,15 @@ export class AccessibilityManager extends Disposable {

constructor(
private readonly _terminal: ITerminal,
@IInstantiationService instantiationService: IInstantiationService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IRenderService private readonly _renderService: IRenderService
) {
super();
this._accessibilityContainer = document.createElement('div');
this._accessibilityContainer = this._coreBrowserService.mainDocument.createElement('div');
this._accessibilityContainer.classList.add('xterm-accessibility');

this._rowContainer = document.createElement('div');
this._rowContainer = this._coreBrowserService.mainDocument.createElement('div');
this._rowContainer.setAttribute('role', 'list');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
Expand All @@ -72,7 +71,7 @@ export class AccessibilityManager extends Disposable {
this._refreshRowsDimensions();
this._accessibilityContainer.appendChild(this._rowContainer);

this._liveRegion = document.createElement('div');
this._liveRegion = this._coreBrowserService.mainDocument.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityContainer.appendChild(this._liveRegion);
Expand All @@ -93,13 +92,7 @@ export class AccessibilityManager extends Disposable {
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));

this._screenDprMonitor = new ScreenDprMonitor(window);
this.register(this._screenDprMonitor);
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the ScreenDprMonitor isn't supported
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions()));

this._refreshRows();
this.register(toDisposable(() => {
Expand Down Expand Up @@ -261,7 +254,7 @@ export class AccessibilityManager extends Disposable {
}

private _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
const element = this._coreBrowserService.mainDocument.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
Expand Down
72 changes: 0 additions & 72 deletions src/browser/ScreenDprMonitor.ts

This file was deleted.

35 changes: 23 additions & 12 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker }
import { WindowsOptionsReportType } from '../common/InputHandler';
import { AccessibilityManager } from './AccessibilityManager';

// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;

export class Terminal extends CoreTerminal implements ITerminal {
public textarea: HTMLTextAreaElement | undefined;
public element: HTMLElement | undefined;
Expand Down Expand Up @@ -397,7 +394,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._logService.debug('Terminal.open was called on an element that was not attached to the DOM');
}

this._document = parent.ownerDocument!;
// If the terminal is already opened
if (this.element?.ownerDocument.defaultView && this._coreBrowserService) {
// Adjust the window if needed
if (this.element.ownerDocument.defaultView !== this._coreBrowserService.window) {
this._coreBrowserService.window = this.element.ownerDocument.defaultView;
}
return;
}

this._document = parent.ownerDocument;
if (this.options.documentOverride && this.options.documentOverride instanceof Document) {
this._document = this.optionsService.rawOptions.documentOverride as Document;
}
Expand All @@ -411,25 +417,25 @@ export class Terminal extends CoreTerminal implements ITerminal {

// Performance: Use a document fragment to build the terminal
// viewport and helper elements detached from the DOM
const fragment = document.createDocumentFragment();
this._viewportElement = document.createElement('div');
const fragment = this._document.createDocumentFragment();
this._viewportElement = this._document.createElement('div');
this._viewportElement.classList.add('xterm-viewport');
fragment.appendChild(this._viewportElement);

this._viewportScrollArea = document.createElement('div');
this._viewportScrollArea = this._document.createElement('div');
this._viewportScrollArea.classList.add('xterm-scroll-area');
this._viewportElement.appendChild(this._viewportScrollArea);

this.screenElement = document.createElement('div');
this.screenElement = this._document.createElement('div');
this.screenElement.classList.add('xterm-screen');
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
this._helperContainer = document.createElement('div');
this._helperContainer = this._document.createElement('div');
this._helperContainer.classList.add('xterm-helpers');
this.screenElement.appendChild(this._helperContainer);
fragment.appendChild(this.screenElement);

this.textarea = document.createElement('textarea');
this.textarea = this._document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
this.textarea.setAttribute('aria-label', Strings.promptLabel);
if (!Browser.isChromeOS) {
Expand All @@ -444,7 +450,12 @@ export class Terminal extends CoreTerminal implements ITerminal {

// Register the core browser service before the generic textarea handlers are registered so it
// handles them first. Otherwise the renderers may use the wrong focus state.
this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea, this._document.defaultView ?? window);
this._coreBrowserService = this.register(this._instantiationService.createInstance(CoreBrowserService,
this.textarea,
parent.ownerDocument.defaultView ?? window,
// Force unsafe null in node.js environment for tests
this._document ?? (typeof window !== 'undefined') ? window.document : null as any
));
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);

this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
Expand All @@ -466,7 +477,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));

this._compositionView = document.createElement('div');
this._compositionView = this._document.createElement('div');
this._compositionView.classList.add('composition-view');
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
this._helperContainer.appendChild(this._compositionView);
Expand Down
5 changes: 5 additions & 0 deletions src/browser/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,16 @@ export class MockCompositionHelper implements ICompositionHelper {
}

export class MockCoreBrowserService implements ICoreBrowserService {
public onDprChange = new EventEmitter<number>().event;
public onWindowChange = new EventEmitter<Window & typeof globalThis, void>().event;
public serviceBrand: undefined;
public isFocused: boolean = true;
public get window(): Window & typeof globalThis {
throw Error('Window object not available in tests');
}
public get mainDocument(): Document {
throw Error('Document object not available in tests');
}
public dpr: number = 1;
}

Expand Down
8 changes: 4 additions & 4 deletions src/browser/decorations/BufferDecorationRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { addDisposableDomListener } from 'browser/Lifecycle';
import { IRenderService } from 'browser/services/Services';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';

Expand All @@ -19,6 +18,7 @@ export class BufferDecorationRenderer extends Disposable {
constructor(
private readonly _screenElement: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IDecorationService private readonly _decorationService: IDecorationService,
@IRenderService private readonly _renderService: IRenderService
) {
Expand All @@ -33,7 +33,7 @@ export class BufferDecorationRenderer extends Disposable {
this._dimensionsChanged = true;
this._queueRefresh();
}));
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh()));
this.register(this._coreBrowserService.onDprChange(() => this._queueRefresh()));
this.register(this._bufferService.buffers.onBufferActivate(() => {
this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt;
}));
Expand Down Expand Up @@ -70,7 +70,7 @@ export class BufferDecorationRenderer extends Disposable {
}

private _createElement(decoration: IInternalDecoration): HTMLElement {
const element = document.createElement('div');
const element = this._coreBrowserService.mainDocument.createElement('div');
element.classList.add('xterm-decoration');
element.classList.toggle('xterm-decoration-top-layer', decoration?.options?.layer === 'top');
element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.css.cell.width)}px`;
Expand Down
17 changes: 8 additions & 9 deletions src/browser/decorations/OverviewRulerRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/

import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
Expand Down Expand Up @@ -52,10 +51,10 @@ export class OverviewRulerRenderer extends Disposable {
@IDecorationService private readonly _decorationService: IDecorationService,
@IRenderService private readonly _renderService: IRenderService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreBrowserService private readonly _coreBrowseService: ICoreBrowserService
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
) {
super();
this._canvas = document.createElement('canvas');
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
this._canvas.classList.add('xterm-decoration-overview-ruler');
this._refreshCanvasDimensions();
this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement);
Expand Down Expand Up @@ -112,7 +111,7 @@ export class OverviewRulerRenderer extends Disposable {
// overview ruler width changed
this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true)));
// device pixel ratio changed
this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => this._queueRefresh(true)));
this.register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
// set the canvas dimensions
this._queueRefresh(true);
}
Expand All @@ -135,11 +134,11 @@ export class OverviewRulerRenderer extends Disposable {
}

private _refreshDrawHeightConstants(): void {
drawHeight.full = Math.round(2 * this._coreBrowseService.dpr);
drawHeight.full = Math.round(2 * this._coreBrowserService.dpr);
// Calculate actual pixels per line
const pixelsPerLine = this._canvas.height / this._bufferService.buffer.lines.length;
// Clamp actual pixels within a range
const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * this._coreBrowseService.dpr);
const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * this._coreBrowserService.dpr);
drawHeight.left = nonFullHeight;
drawHeight.center = nonFullHeight;
drawHeight.right = nonFullHeight;
Expand All @@ -157,9 +156,9 @@ export class OverviewRulerRenderer extends Disposable {

private _refreshCanvasDimensions(): void {
this._canvas.style.width = `${this._width}px`;
this._canvas.width = Math.round(this._width * this._coreBrowseService.dpr);
this._canvas.width = Math.round(this._width * this._coreBrowserService.dpr);
this._canvas.style.height = `${this._screenElement.clientHeight}px`;
this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowseService.dpr);
this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowserService.dpr);
this._refreshDrawConstants();
this._refreshColorZonePadding();
}
Expand Down Expand Up @@ -211,7 +210,7 @@ export class OverviewRulerRenderer extends Disposable {
if (this._animationFrame !== undefined) {
return;
}
this._animationFrame = this._coreBrowseService.window.requestAnimationFrame(() => {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._refreshDecorations();
this._animationFrame = undefined;
});
Expand Down
2 changes: 1 addition & 1 deletion src/browser/renderer/shared/CustomGlyphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ function drawPatternChar(
if (!pattern) {
const width = charDefinition[0].length;
const height = charDefinition.length;
const tmpCanvas = document.createElement('canvas');
const tmpCanvas = ctx.canvas.ownerDocument.createElement('canvas');
tmpCanvas.width = width;
tmpCanvas.height = height;
const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
Expand Down
Loading