From ced06629d45129115fbb7e59de8dbd5a6eb16680 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 14 Mar 2022 15:11:20 -0400 Subject: [PATCH] large refactor, broken --- .../Decorations/BufferDecorationRenderer.ts | 162 ++++++++ .../Decorations/OverviewRulerRenderer.ts | 150 ++++++++ src/browser/Terminal.ts | 28 +- src/browser/Types.d.ts | 6 + src/browser/services/DecorationService.ts | 361 ------------------ src/browser/services/Services.ts | 5 +- src/browser/tsconfig.json | 2 +- src/common/services/DecorationService.ts | 81 ++++ src/common/services/Services.ts | 9 +- 9 files changed, 431 insertions(+), 373 deletions(-) create mode 100644 src/browser/Decorations/BufferDecorationRenderer.ts create mode 100644 src/browser/Decorations/OverviewRulerRenderer.ts delete mode 100644 src/browser/services/DecorationService.ts create mode 100644 src/common/services/DecorationService.ts diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/Decorations/BufferDecorationRenderer.ts new file mode 100644 index 00000000..56559ad9 --- /dev/null +++ b/src/browser/Decorations/BufferDecorationRenderer.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IDecorationService, IRenderService } from 'browser/services/Services'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferService } from 'common/services/Services'; +import { IMarker } from 'common/Types'; +import { IDecoration, IDecorationOptions } from 'xterm'; + +export interface IDecorationRenderer { + refreshDecorations(shouldRecreate?: boolean): void; + renderDecoration(decoration: IDecoration, decorationOptions: IDecorationOptions): void; +} + +export class BufferDecorationRenderer extends Disposable implements IDecorationRenderer { + private _decorationContainer: HTMLElement; + private readonly _decorations: BufferDecoration[] = []; + private _altBufferIsActive: boolean = false; + + constructor( + @IBufferService private readonly _bufferService: IBufferService, + @IRenderService private readonly _renderService: IRenderService, + private readonly _decorationService: IDecorationService, + private readonly _screenElement: HTMLElement) { + super(); + this._decorationContainer = document.createElement('div'); + this._decorationContainer.classList.add('xterm-decoration-container'); + this._screenElement.appendChild(this._decorationContainer); + this.register(this._renderService.onRenderedBufferChange(() => this.refreshDecorations())); + this.register(this._renderService.onDimensionsChange(() => this.refreshDecorations())); + this.register(addDisposableDomListener(window, 'resize', () => this.refreshDecorations())); + this.register(this._bufferService.buffers.onBufferActivate(() => { + this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt; + })); + this.register(this._decorationService.onDecorationRegistered(options => this.renderDecoration(options))); + } + public refreshDecorations(shouldRecreate?: boolean): void { + if (!this._renderService) { + return; + } + for (const decoration of this._decorations) { + decoration.render(this._decorationContainer, this._renderService, shouldRecreate); + } + } + + public renderDecoration(decorationOptions: IDecorationOptions): void { + const decoration = new BufferDecoration(this._bufferService, decorationOptions); + if (this._decorationContainer && decoration.element && !this._decorationContainer.contains(decoration.element)) { + this._decorationContainer.append(decoration.element); + } + (decoration as BufferDecoration).render(this._decorationContainer, this._renderService, true); + } + + public override dispose(): void { + if (this._screenElement && this._decorationContainer && this._screenElement.contains(this._decorationContainer)) { + this._screenElement.removeChild(this._decorationContainer); + } + for (const bufferDecoration of this._decorations) { + bufferDecoration.dispose(); + } + super.dispose(); + } +} +export class BufferDecoration extends Disposable implements IDecoration { + private readonly _marker: IMarker; + private _element: HTMLElement | undefined; + private _container: HTMLElement | undefined; + private _altBufferIsActive: boolean = false; + + public isDisposed: boolean = false; + + public get element(): HTMLElement | undefined { return this._element; } + public get marker(): IMarker { return this._marker; } + + private _onDispose = new EventEmitter(); + public get onDispose(): IEvent { return this._onDispose.event; } + + private _onRender = new EventEmitter(); + public get onRender(): IEvent { return this._onRender.event; } + + + public x: number; + public anchor: 'left' | 'right'; + public width: number; + public height: number; + + constructor( + private readonly _bufferService: IBufferService, + options: IDecorationOptions + ) { + super(); + this.x = options.x ?? 0; + this._marker = options.marker; + this._marker.onDispose(() => this.dispose()); + this.anchor = options.anchor || 'left'; + this.width = options.width || 1; + this.height = options.height || 1; + } + + public render(container: HTMLElement, renderService: IRenderService, shouldRecreate?: boolean): void { + this._container = container; + if (!this._element || shouldRecreate) { + this._createElement(renderService, shouldRecreate); + } + this._refreshStyle(renderService); + if (this._element) { + this._onRender.fire(this._element); + } + } + + private _createElement(renderService: IRenderService, shouldRecreate?: boolean): void { + if (shouldRecreate && this._element && this._container && this._container.contains(this._element)) { + this._container.removeChild(this._element); + } + this._element = document.createElement('div'); + this._element.classList.add('xterm-decoration'); + this._element.style.width = `${this.width * renderService.dimensions.actualCellWidth}px`; + this._element.style.height = `${this.height * renderService.dimensions.actualCellHeight}px`; + this._element.style.top = `${(this.marker.line - this._bufferService.buffers.active.ydisp) * renderService.dimensions.actualCellHeight}px`; + this._element.style.lineHeight = `${renderService.dimensions.actualCellHeight}px`; + + if (this.x && this.x > this._bufferService.cols) { + // exceeded the container width, so hide + this._element.style.display = 'none'; + } + if (this.anchor === 'right') { + this._element.style.right = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; + } else { + this._element.style.left = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; + } + } + + private _refreshStyle(renderService: IRenderService): void { + if (!this._element) { + return; + } + const line = this.marker.line - this._bufferService.buffers.active.ydisp; + if (line < 0 || line > this._bufferService.rows) { + // outside of viewport + this._element.style.display = 'none'; + } else { + this._element.style.top = `${line * renderService.dimensions.actualCellHeight}px`; + this._element.style.display = this._altBufferIsActive ? 'none' : 'block'; + } + } + + public override dispose(): void { + if (this.isDisposed || !this._container) { + return; + } + if (this._element && this._container.contains(this._element)) { + this._container.removeChild(this._element); + } + this.isDisposed = true; + this._onDispose.fire(); + } +} + diff --git a/src/browser/Decorations/OverviewRulerRenderer.ts b/src/browser/Decorations/OverviewRulerRenderer.ts new file mode 100644 index 00000000..580a831a --- /dev/null +++ b/src/browser/Decorations/OverviewRulerRenderer.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IDecorationRenderer } from 'browser/Decorations/BufferDecorationRenderer'; +import { IDecorationService, IRenderService } from 'browser/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferService, IInstantiationService } from 'common/services/Services'; +import { IDecorationOptions, IDecoration, IMarker } from 'xterm'; +const enum ScrollbarConstants { + WIDTH = 7 +} + +export class OverviewRulerRenderer extends Disposable implements IDecorationRenderer { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D | null; + private _decorations: ScrollbarDecoration[] = []; + private _width: number | undefined; + private _anchor: 'right' | 'left' | undefined; + private _x: number | undefined; + + constructor( + @IBufferService private readonly _bufferService: IBufferService, + @IRenderService private readonly _renderService: IRenderService, + @IDecorationService private readonly _decorationService: IDecorationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + private readonly _viewportElement: HTMLElement, + private readonly _screenElement: HTMLElement + ) { + super(); + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('xterm-decoration-scrollbar'); + this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement); + this._ctx = this._canvas.getContext('2d'); + this.refreshDecorations(); + this.register(this._bufferService.buffers.onBufferActivate(() => { + this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; + })); + this.register(this._renderService.onRenderedBufferChange(() => this.refreshDecorations())); + this.register(this._renderService.onDimensionsChange(() => this.refreshDecorations())); + this.register(addDisposableDomListener(window, 'resize', () => this.refreshDecorations())); + this.register(this._decorationService.onDecorationRegistered(e => this.renderDecoration(e))); + this.register(this._decorationService.onDecorationRemoved(d => d.dispose())); + } + public renderDecoration(decorationOptions: IDecorationOptions): void { + if (!this._ctx || !decorationOptions.overviewRulerItemColor) { + return; + } + const decoration = this._instantiationService.createInstance(ScrollbarDecoration, { marker: decorationOptions.marker, overviewRulerItemColor: decorationOptions.overviewRulerItemColor }); + + this._ctx.lineWidth = 1; + this._ctx.strokeStyle = decorationOptions.overviewRulerItemColor; + this._ctx.strokeRect( + 0, + Math.round(this._canvas.height * (decoration.marker.line / this._bufferService.buffers.active.lines.length)), + this._canvas.width, + window.devicePixelRatio + ); + } + + public refreshDecorations(): void { + if (!this._ctx) { + return; + } + this._canvas.style.width = `${this._width || ScrollbarConstants.WIDTH}px`; + this._canvas.style.height = `${this._screenElement.clientHeight}px`; + this._canvas.width = Math.floor((this._width || ScrollbarConstants.WIDTH)* window.devicePixelRatio); + this._canvas.height = Math.floor(this._screenElement.clientHeight * window.devicePixelRatio); + if (this._anchor === 'right') { + this._canvas.style.right = this._x ? `${this._x * this._renderService.dimensions.actualCellWidth}px` : ''; + } else { + this._canvas.style.left = this._x ? `${this._x * this._renderService.dimensions.actualCellWidth}px` : ''; + } + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + for (const decoration of this._decorations) { + decoration.render(this._ctx, this._canvas); + } + } + public override dispose(): void { + for (const decoration of this._decorations) { + decoration.dispose(); + } + this._decorations = []; + this._canvas?.remove(); + super.dispose(); + } +} +class ScrollbarDecoration extends Disposable implements IDecoration { + private readonly _marker: IMarker; + private _ctx: CanvasRenderingContext2D | undefined; + private _canvas: HTMLCanvasElement | undefined; + private _color: string | undefined; + + public isDisposed: boolean = false; + + public get element(): HTMLCanvasElement | undefined { return this._canvas; } + public get marker(): IMarker { return this._marker; } + public get color(): string | undefined { return this._color; } + + private _onDispose = new EventEmitter(); + public get onDispose(): IEvent { return this._onDispose.event; } + + private _onRender = new EventEmitter(); + public get onRender(): IEvent { return this._onRender.event; } + + constructor( + options: IDecorationOptions, + @IBufferService private readonly _bufferService: IBufferService + ) { + super(); + this._marker = options.marker; + this._color = options.overviewRulerItemColor; + this._marker.onDispose(() => this.dispose()); + } + + public render(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement): void { + if (!this.color) { + throw new Error('No color was provided for the overview ruler decoraiton'); + } + this._ctx = ctx; + this._canvas = canvas; + ctx.lineWidth = 1; + ctx.strokeStyle = this.color; + ctx.strokeRect( + 0, + Math.round(canvas.height * (this.marker.line / this._bufferService.buffers.active.lines.length)), + canvas.width, + window.devicePixelRatio + ); + this._onRender.fire(canvas); + } + + public override dispose(): void { + if (this._isDisposed || !this._canvas || !this._ctx) { + return; + } + this._ctx.clearRect( + 0, + Math.round(this._canvas.height * (this.marker.line / this._bufferService.buffers.active.lines.length)), + this._canvas.width, + window.devicePixelRatio + ); + this.isDisposed = true; + this._onDispose.fire(); + super.dispose(); + } +} diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index ba0fc93d..39f0a68a 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -45,7 +45,7 @@ import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService, IDecorationService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { IBuffer } from 'common/buffer/Types'; import { MouseService } from 'browser/services/MouseService'; @@ -55,7 +55,10 @@ import { CoreTerminal } from 'common/CoreTerminal'; import { color, rgba } from 'browser/Color'; import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; import { toRgbString } from 'common/input/XParseColor'; -import { DecorationService } from 'browser/services/DecorationService'; +import { BufferDecorationRenderer } from 'browser/Decorations/BufferDecorationRenderer'; +import { OverviewRulerRenderer } from 'browser/Decorations/OverviewRulerRenderer'; +// import { IDecorationService } from 'common/services/Services'; +import { DecorationService } from 'common/services/DecorationService'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -71,6 +74,9 @@ export class Terminal extends CoreTerminal implements ITerminal { private _helperContainer: HTMLElement | undefined; private _compositionView: HTMLElement | undefined; + private _overviewRulerRenderer: OverviewRulerRenderer | undefined; + private _bufferDecorationRenderer: BufferDecorationRenderer | undefined; + // private _visualBellTimer: number; public browser: IBrowser = Browser as any; @@ -81,6 +87,7 @@ export class Terminal extends CoreTerminal implements ITerminal { private _charSizeService: ICharSizeService | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; + private _decorationService: DecorationService | undefined; private _characterJoinerService: ICharacterJoinerService | undefined; private _selectionService: ISelectionService | undefined; private _soundService: ISoundService | undefined; @@ -109,7 +116,6 @@ export class Terminal extends CoreTerminal implements ITerminal { public linkifier: ILinkifier; public linkifier2: ILinkifier2; public viewport: IViewport | undefined; - public decorationService: IDecorationService; private _compositionHelper: ICompositionHelper | undefined; private _mouseZoneManager: IMouseZoneManager | undefined; private _accessibilityManager: AccessibilityManager | undefined; @@ -159,7 +165,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.linkifier = this._instantiationService.createInstance(Linkifier); this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2)); - this.decorationService = this.register(this._instantiationService.createInstance(DecorationService)); + this._decorationService = this._instantiationService.createInstance(DecorationService); // Setup InputHandler listeners this.register(this._inputHandler.onRequestBell(() => this.bell())); @@ -577,8 +583,16 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this.onScroll(() => this._mouseZoneManager!.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService); - - this.decorationService.attachToDom(this._renderService, this.screenElement, this._viewportElement); + if (this._decorationService) { + this._bufferDecorationRenderer = new BufferDecorationRenderer(this._bufferService, this._renderService, this._decorationService, this.screenElement); + } + // if (this.options.overviewRulerWidth && this._decorationService) { + // this._overviewRulerRenderer = new OverviewRulerRenderer(this._bufferService, this._renderService, this._decorationService, this._viewportElement, this.screenElement); + // } + // this.optionsService.onOptionChange(() => { + // if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._renderService && this._viewportElement && this.screenElement && this._decorationService) { + // this._overviewRulerRenderer = new OverviewRulerRenderer(this._bufferService, this._renderService, this._decorationService, this._viewportElement, this.screenElement); + // }}); // This event listener must be registered aftre MouseZoneManager is created this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e))); @@ -1004,7 +1018,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - return this.decorationService!.registerDecoration(decorationOptions); + return this._decorationService!.registerDecoration(decorationOptions); } /** diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 35b52d62..b8e1626e 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -9,6 +9,7 @@ import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IFunctionIdentifier, IParams } from 'common/parser/Types'; +import { createDecorator } from 'common/services/ServiceRegistry'; export interface ITerminal extends IPublicTerminal, ICoreTerminal { element: HTMLElement | undefined; @@ -205,6 +206,11 @@ export interface ILinkifier { registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): boolean; } +export interface IDecorationService extends IDisposable { + readonly onDecorationRegistered: IEvent; + readonly onDecorationRemoved: IEvent; + registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; +} interface ILinkState { decorations: ILinkDecorations; diff --git a/src/browser/services/DecorationService.ts b/src/browser/services/DecorationService.ts deleted file mode 100644 index 181588cd..00000000 --- a/src/browser/services/DecorationService.ts +++ /dev/null @@ -1,361 +0,0 @@ -/** - * Copyright (c) 2022 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IDecorationService, IRenderService } from 'browser/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; -import { IBufferService, IInstantiationService } from 'common/services/Services'; -import { IDecorationOptions, IDecoration, IMarker } from 'xterm'; - -const enum ScrollbarConstants { - WIDTH = 7 -} - -interface IDecorationRenderer { - refreshDecorations(shouldRecreate?: boolean): void; - registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; -} - -class BufferDecorationRenderer extends Disposable implements IDecorationRenderer { - private _decorationContainer: HTMLElement | undefined; - private readonly _decorations: BufferDecoration[] = []; - private _renderService: IRenderService | undefined; - - constructor( - @IBufferService private readonly _bufferService: IBufferService, - private readonly _screenElement: HTMLElement) { - super(); - } - public refreshDecorations(shouldRecreate?: boolean): void { - if (!this._renderService) { - return; - } - for (const decoration of this._decorations) { - decoration.render(this._renderService, shouldRecreate); - } - } - public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - if (this._screenElement && !this._decorationContainer) { - this._decorationContainer = document.createElement('div'); - this._decorationContainer.classList.add('xterm-decoration-container'); - this._screenElement.appendChild(this._decorationContainer); - } - const decoration = new BufferDecoration(this._bufferService, decorationOptions, this._decorationContainer); - this._decorations.push(decoration); - decoration.onDispose(() => this._decorations.splice(this._decorations.indexOf(decoration), 1)); - this.refreshDecorations(); - return decoration; - } - public override dispose(): void { - if (this._screenElement && this._decorationContainer && this._screenElement.contains(this._decorationContainer)) { - this._screenElement.removeChild(this._decorationContainer); - } - for (const bufferDecoration of this._decorations) { - bufferDecoration.dispose(); - } - super.dispose(); - } - public attachToDom(renderService: IRenderService): void { - this._renderService = renderService; - } -} - -class OverviewRulerRenderer extends Disposable implements IDecorationRenderer { - private _canvas: HTMLCanvasElement | undefined; - private _ctx: CanvasRenderingContext2D | null = null; - private _decorations: ScrollbarDecoration[] = []; - private _width: number | undefined; - private _anchor: 'right' | 'left' | undefined; - private _x: number | undefined; - - constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IBufferService private readonly _bufferService: IBufferService, - @IRenderService private readonly _renderService: IRenderService, - private readonly _viewportElement: HTMLElement, - private readonly _screenElement: HTMLElement - ) { - super(); - this.register(this._bufferService.buffers.onBufferActivate(() => { - this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; - })); - } - public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - if (!this._viewportElement.parentElement) { - return; - } - if (!this._canvas) { - this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-decoration-scrollbar'); - this._viewportElement.parentElement.insertBefore(this._canvas, this._viewportElement); - } - this._width = decorationOptions.width; - this._anchor = decorationOptions.anchor; - this._x = decorationOptions.x; - - if (!this._ctx) { - this._ctx = this._canvas.getContext('2d'); - this.refreshDecorations(); - } - const decoration = this._instantiationService.createInstance(ScrollbarDecoration, { marker: decorationOptions.marker, overviewRulerItemColor: decorationOptions.overviewRulerItemColor }, this._canvas, this._ctx!); - decoration.onDispose(() => this._decorations.splice(this._decorations.indexOf(decoration), 1)); - this._decorations.push(decoration); - return decoration; - } - public refreshDecorations(): void { - if (!this._canvas || !this._ctx || !this._screenElement || !this._renderService) { - return; - } - this._canvas.style.width = `${this._width || ScrollbarConstants.WIDTH}px`; - this._canvas.style.height = `${this._screenElement.clientHeight}px`; - this._canvas.width = Math.floor((this._width || ScrollbarConstants.WIDTH)* window.devicePixelRatio); - this._canvas.height = Math.floor(this._screenElement.clientHeight * window.devicePixelRatio); - if (this._anchor === 'right') { - this._canvas.style.right = this._x ? `${this._x * this._renderService.dimensions.actualCellWidth}px` : ''; - } else { - this._canvas.style.left = this._x ? `${this._x * this._renderService.dimensions.actualCellWidth}px` : ''; - } - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - for (const decoration of this._decorations) { - decoration.render(); - } - } - public override dispose(): void { - for (const decoration of this._decorations) { - decoration.dispose(); - } - this._decorations = []; - this._canvas?.remove(); - super.dispose(); - } -} - -export class DecorationService extends Disposable implements IDecorationService { - - private _renderService: IRenderService | undefined; - private _animationFrame: number | undefined; - - private _screenElement: HTMLElement | undefined; - private _viewportElement: HTMLElement | undefined; - - private _overviewRulerRenderer: OverviewRulerRenderer | undefined; - private _bufferDecorationRenderer: BufferDecorationRenderer | undefined; - - constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService, @IBufferService private readonly _bufferService: IBufferService) { - super(); - } - - public attachToDom(renderService: IRenderService, screenElement: HTMLElement, viewportElement: HTMLElement): void { - this._renderService = renderService; - this._screenElement = screenElement; - this._viewportElement = viewportElement; - this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); - this.register(this._renderService.onDimensionsChange(() => this._refresh(true))); - this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh())); - if (!this._bufferDecorationRenderer && this._viewportElement && this._screenElement) { - // TODO: allow registering before the viewport element exists - this._bufferDecorationRenderer = new BufferDecorationRenderer(this._bufferService, this._screenElement); - this._bufferDecorationRenderer.attachToDom(renderService); - } - } - - public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - if (decorationOptions.marker.isDisposed || !this._renderService) { - return undefined; - } - - if (decorationOptions.overviewRulerItemColor && this._viewportElement && this._screenElement) { - if (!this._overviewRulerRenderer) { - this._overviewRulerRenderer = new OverviewRulerRenderer(this._instantiationService, this._bufferService, this._renderService, this._viewportElement, this._screenElement); - } - return this._overviewRulerRenderer.registerDecoration(decorationOptions); - } - return this._bufferDecorationRenderer?.registerDecoration(decorationOptions); - } - - public dispose(): void { - this._overviewRulerRenderer?.dispose(); - this._bufferDecorationRenderer?.dispose(); - } - - private _refresh(shouldRecreate?: boolean): void { - this._bufferDecorationRenderer?.refreshDecorations(shouldRecreate); - this._overviewRulerRenderer?.refreshDecorations(); - } - - private _queueRefresh(): void { - if (this._animationFrame !== undefined) { - return; - } - this._animationFrame = window.requestAnimationFrame(() => { - this._refresh(); - this._animationFrame = undefined; - }); - } -} - -export class ScrollbarDecoration extends Disposable implements IDecoration { - private readonly _marker: IMarker; - private _element: HTMLCanvasElement | undefined; - private _color: string | undefined; - - public isDisposed: boolean = false; - - public get element(): HTMLCanvasElement | undefined { return this._element; } - public get marker(): IMarker { return this._marker; } - public get color(): string | undefined { return this._color; } - - private _onDispose = new EventEmitter(); - public get onDispose(): IEvent { return this._onDispose.event; } - - private _onRender = new EventEmitter(); - public get onRender(): IEvent { return this._onRender.event; } - - constructor( - options: IDecorationOptions, - private readonly _canvas: HTMLCanvasElement, - private readonly _ctx: CanvasRenderingContext2D, - @IBufferService private readonly _bufferService: IBufferService - ) { - super(); - this._marker = options.marker; - this._color = options.overviewRulerItemColor; - this._marker.onDispose(() => this.dispose()); - this.render(); - } - public render(): void { - if (!this.color) { - throw new Error('No color was provided for the overview ruler decoraiton'); - } - if (!this._element) { - this._element = this._canvas; - } - this._ctx.lineWidth = 1; - this._ctx.strokeStyle = this.color; - this._ctx.strokeRect( - 0, - Math.round(this._element.height * (this.marker.line / this._bufferService.buffers.active.lines.length)), - this._element.width, - window.devicePixelRatio - ); - this._onRender.fire(this._element); - } - - public override dispose(): void { - if (this._isDisposed || !this._element) { - return; - } - this._ctx.clearRect( - 0, - Math.round(this._element.height * (this.marker.line / this._bufferService.buffers.active.lines.length)), - this._element.width, - window.devicePixelRatio - ); - this.isDisposed = true; - this._onDispose.fire(); - super.dispose(); - } -} - -export class BufferDecoration extends Disposable implements IDecoration { - private readonly _marker: IMarker; - private _element: HTMLElement | undefined; - - public isDisposed: boolean = false; - - public get element(): HTMLElement | undefined { return this._element; } - public get marker(): IMarker { return this._marker; } - - private _onDispose = new EventEmitter(); - public get onDispose(): IEvent { return this._onDispose.event; } - - private _onRender = new EventEmitter(); - public get onRender(): IEvent { return this._onRender.event; } - - private _altBufferIsActive: boolean = false; - - public x: number; - public anchor: 'left' | 'right'; - public width: number; - public height: number; - - constructor( - private readonly _bufferService: IBufferService, - options: IDecorationOptions, - private readonly _container?: HTMLElement - ) { - super(); - this.x = options.x ?? 0; - this._marker = options.marker; - this._marker.onDispose(() => this.dispose()); - this.anchor = options.anchor || 'left'; - this.width = options.width || 1; - this.height = options.height || 1; - this.register(this._bufferService.buffers.onBufferActivate(() => { - this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt; - })); - } - - public render(renderService: IRenderService, shouldRecreate?: boolean): void { - if (!this._element || shouldRecreate) { - this._createElement(renderService, shouldRecreate); - } - if (this._container && this._element && !this._container.contains(this._element)) { - this._container.append(this._element); - } - this._refreshStyle(renderService); - if (this._element) { - this._onRender.fire(this._element); - } - } - - private _createElement(renderService: IRenderService, shouldRecreate?: boolean): void { - if (shouldRecreate && this._element && this._container && this._container.contains(this._element)) { - this._container.removeChild(this._element); - } - this._element = document.createElement('div'); - this._element.classList.add('xterm-decoration'); - this._element.style.width = `${this.width * renderService.dimensions.actualCellWidth}px`; - this._element.style.height = `${this.height * renderService.dimensions.actualCellHeight}px`; - this._element.style.top = `${(this.marker.line - this._bufferService.buffers.active.ydisp) * renderService.dimensions.actualCellHeight}px`; - this._element.style.lineHeight = `${renderService.dimensions.actualCellHeight}px`; - - if (this.x && this.x > this._bufferService.cols) { - // exceeded the container width, so hide - this._element.style.display = 'none'; - } - if (this.anchor === 'right') { - this._element.style.right = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; - } else { - this._element.style.left = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; - } - } - - private _refreshStyle(renderService: IRenderService): void { - if (!this._element) { - return; - } - const line = this.marker.line - this._bufferService.buffers.active.ydisp; - if (line < 0 || line > this._bufferService.rows) { - // outside of viewport - this._element.style.display = 'none'; - } else { - this._element.style.top = `${line * renderService.dimensions.actualCellHeight}px`; - this._element.style.display = this._altBufferIsActive ? 'none' : 'block'; - } - } - - public override dispose(): void { - if (this.isDisposed || !this._container) { - return; - } - if (this._element && this._container.contains(this._element)) { - this._container.removeChild(this._element); - } - this.isDisposed = true; - this._onDispose.fire(); - } -} diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 2d1f3d03..8587a5c9 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -115,10 +115,9 @@ export interface ICharacterJoinerService { deregister(joinerId: number): boolean; getJoinedCharacters(row: number): [number, number][]; } - - export const IDecorationService = createDecorator('DecorationService'); export interface IDecorationService extends IDisposable { - attachToDom(renderService: IRenderService, screenElement: HTMLElement, viewportElement: HTMLElement): void; + readonly onDecorationRegistered: IEvent; + readonly onDecorationRemoved: IEvent; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; } diff --git a/src/browser/tsconfig.json b/src/browser/tsconfig.json index 212aeab8..fc997477 100644 --- a/src/browser/tsconfig.json +++ b/src/browser/tsconfig.json @@ -18,7 +18,7 @@ "include": [ "./**/*", "../../typings/xterm.d.ts" - ], +], "references": [ { "path": "../common" } ] diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts new file mode 100644 index 00000000..206d50e8 --- /dev/null +++ b/src/common/services/DecorationService.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +import { EventEmitter } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; +import { IDecorationOptions, IDecoration, IMarker, IDisposable, IEvent } from 'xterm'; + +export class DecorationService extends Disposable implements IDecorationService { + private _animationFrame: number | undefined; + private _onDecorationRegistered = this.register(new EventEmitter()); + public get onDecorationRegistered(): IEvent { return this._onDecorationRegistered.event; } + private _onDecorationRemoved = this.register(new EventEmitter()); + public get onDecorationRemoved(): IEvent { return this._onDecorationRemoved.event; } + private _decorations: IDecoration[] = []; + + constructor() { + super(); + } + + public registerDecoration(options: IDecorationOptions): IDecoration | undefined { + if (options.marker.isDisposed) { + return undefined; + } + const decoration = new Decoration(options); + if (decoration) { + decoration.onDispose(() => { + if (decoration) { + this._decorations.splice(this._decorations.indexOf(decoration), 1); + } + }); + this._decorations.push(decoration); + this._onDecorationRegistered.fire(options); + decoration.onRender(d => { + decoration.setElement(d); + }); + } + return decoration; + } + + public dispose(): void { + for (const decoration of this._decorations) { + this._onDecorationRemoved.fire(decoration); + decoration.dispose(); + } + this._decorations = []; + } + + private _queueRefresh(): void { + if (this._animationFrame !== undefined) { + return; + } + this._animationFrame = window.requestAnimationFrame(() => { + // this._refresh(); + this._animationFrame = undefined; + }); + } +} + +class Decoration implements IDecoration { + public marker: IMarker; + private _onRender = new EventEmitter(); + public get onRender(): IEvent { return this._onRender.event; } + private _onDispose = new EventEmitter(); + public get onDispose(): IEvent { return this._onDispose.event; } + public element: HTMLElement | undefined; + public isDisposed: boolean = false; + public dispose(): void { + throw new Error('Method not implemented.'); + } + constructor(decorationOptions?: IDecorationOptions) { + this.marker = decorationOptions?.marker!; + this.element = undefined; + } + public setElement(element: HTMLElement): void { + this.element = element; + } +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 90dca988..8bd272f0 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,8 +5,9 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; +import { IDecorationOptions, IDecoration } from 'xterm'; export const IBufferService = createDecorator('BufferService'); export interface IBufferService { @@ -245,6 +246,7 @@ export interface ITerminalOptions { windowsMode: boolean; windowOptions: IWindowOptions; wordSeparator: string; + overviewRulerWidth?: number; [key: string]: any; cancelEvents: boolean; @@ -298,3 +300,8 @@ export interface IUnicodeVersionProvider { readonly version: string; wcwidth(ucs: number): 0 | 1 | 2; } +export interface IDecorationService extends IDisposable { + readonly onDecorationRegistered: IEvent; + readonly onDecorationRemoved: IEvent; + registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; +}