This commit is contained in:
meganrogge
2022-03-16 11:39:34 -04:00
16 changed files with 613 additions and 201 deletions
@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* 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 { IRenderService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
export class BufferDecorationRenderer extends Disposable {
private readonly _container: HTMLElement;
private readonly _decorationElements: Map<IInternalDecoration, HTMLElement> = new Map();
private _animationFrame: number | undefined;
private _altBufferIsActive: boolean = false;
constructor(
private readonly _screenElement: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IDecorationService private readonly _decorationService: IDecorationService,
@IRenderService private readonly _renderService: IRenderService
) {
super();
this._container = document.createElement('div');
this._container.classList.add('xterm-decoration-container');
this._screenElement.appendChild(this._container);
this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh()));
this.register(this._renderService.onDimensionsChange(() => this._queueRefresh()));
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh()));
this.register(this._bufferService.buffers.onBufferActivate(() => {
this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt;
}));
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh()));
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
}
public override dispose(): void {
this._container.remove();
this._decorationElements.clear();
super.dispose();
}
private _queueRefresh(): void {
if (this._animationFrame !== undefined) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => {
this.refreshDecorations();
this._animationFrame = undefined;
});
}
public refreshDecorations(): void {
for (const decoration of this._decorationService.decorations) {
this._renderDecoration(decoration);
}
}
private _renderDecoration(decoration: IInternalDecoration): void {
let element = this._decorationElements.get(decoration);
if (!element) {
element = this._createElement(decoration);
decoration.onDispose(() => this._removeDecoration(decoration));
decoration.marker.onDispose(() => decoration.dispose());
decoration.element = element;
this._decorationElements.set(decoration, element);
this._container.appendChild(element);
}
this._refreshStyle(decoration, element);
decoration.onRenderEmitter.fire(element);
}
private _createElement(decoration: IInternalDecoration): HTMLElement {
const element = document.createElement('div');
element.classList.add('xterm-decoration');
element.style.width = `${(decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth}px`;
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`;
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`;
element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`;
const x = decoration.options.x ?? 0;
if (x && x > this._bufferService.cols) {
// exceeded the container width, so hide
element.style.display = 'none';
}
if ((decoration.options.anchor || 'left') === 'right') {
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
} else {
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
}
return element;
}
private _refreshStyle(decoration: IInternalDecoration, element: HTMLElement): void {
const line = decoration.marker.line - this._bufferService.buffers.active.ydisp;
if (line < 0 || line > this._bufferService.rows) {
// outside of viewport
element.style.display = 'none';
} else {
element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`;
element.style.display = this._altBufferIsActive ? 'none' : 'block';
}
}
private _removeDecoration(decoration: IInternalDecoration): void {
this._decorationElements.get(decoration)?.remove();
this._decorationElements.delete(decoration);
}
}
@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* 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 { IRenderService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services';
// This is used to reduce memory usage
// when refreshStyle is called
// by storing and updating
// the sizes of the decorations to be drawn
const renderSizes = new Uint16Array(3);
const enum SizeIndex {
OUTER_SIZE = 0,
INNER_SIZE = 0
}
export class OverviewRulerRenderer extends Disposable {
private readonly _canvas: HTMLCanvasElement;
private readonly _ctx: CanvasRenderingContext2D;
private readonly _decorationElements: Map<IInternalDecoration, HTMLElement> = new Map();
private get _width(): number {
return this._optionsService.options.overviewRulerWidth || 0;
}
private _animationFrame: number | undefined;
constructor(
private readonly _viewportElement: HTMLElement,
private readonly _screenElement: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IDecorationService private readonly _decorationService: IDecorationService,
@IRenderService private readonly _renderService: IRenderService,
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._canvas = document.createElement('canvas');
this._canvas.classList.add('xterm-decoration-overview-ruler');
this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement);
const ctx = this._canvas.getContext('2d');
if (!ctx) {
throw new Error('Ctx cannot be null');
} else {
this._ctx = ctx;
}
this._queueRefresh(true);
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._queueRefresh()));
this.register(this._renderService.onDimensionsChange(() => this._queueRefresh(true, true)));
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh(true)));
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh(undefined, true)));
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
this.register(this._optionsService.onOptionChange(o => {
if (o === 'overviewRulerWidth') {
renderSizes[SizeIndex.OUTER_SIZE] = Math.floor(this._width / 3);
renderSizes[SizeIndex.INNER_SIZE] = Math.ceil(this._width / 3);
this._queueRefresh();
}
}));
renderSizes[SizeIndex.OUTER_SIZE] = Math.floor(this._width / 3);
renderSizes[SizeIndex.INNER_SIZE] = Math.ceil(this._width / 3);
}
public override dispose(): void {
for (const decoration of this._decorationElements) {
this._ctx?.clearRect(
0,
Math.round(this._canvas.height * (decoration[0].marker.line / this._bufferService.buffers.active.lines.length)),
this._canvas.width,
window.devicePixelRatio
);
}
this._decorationElements.clear();
this._canvas?.remove();
super.dispose();
}
private _refreshStyle(decoration: IInternalDecoration, updateAnchor?: boolean): void {
if (updateAnchor) {
if (decoration.options.anchor === 'right') {
this._canvas.style.right = decoration.options.x ? `${decoration.options.x * this._renderService.dimensions.actualCellWidth}px` : '';
} else {
this._canvas.style.left = decoration.options.x ? `${decoration.options.x * this._renderService.dimensions.actualCellWidth}px` : '';
}
}
if (!decoration.options.overviewRulerOptions) {
this._decorationElements.delete(decoration);
return;
}
this._ctx.lineWidth = !decoration.options.overviewRulerOptions.position ? 2 : 6;
this._ctx.strokeStyle = decoration.options.overviewRulerOptions.color;
this._ctx.strokeRect(
!decoration.options.overviewRulerOptions.position || decoration.options.overviewRulerOptions.position === 'left' ? 0 : decoration.options.overviewRulerOptions.position === 'right' ? renderSizes[SizeIndex.OUTER_SIZE] + renderSizes[SizeIndex.INNER_SIZE]: renderSizes[SizeIndex.OUTER_SIZE],
Math.round(this._canvas.height * (decoration.options.marker.line / this._bufferService.buffers.active.lines.length)),
!decoration.options.overviewRulerOptions.position ? this._width : decoration.options.overviewRulerOptions.position === 'center' ? renderSizes[SizeIndex.INNER_SIZE]: renderSizes[SizeIndex.OUTER_SIZE],
window.devicePixelRatio
);
}
private _refreshDecorations(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void {
if (updateCanvasDimensions) {
this._canvas.style.width = `${this._width}px`;
this._canvas.style.height = `${this._screenElement.clientHeight}px`;
this._canvas.width = Math.floor((this._width)* window.devicePixelRatio);
this._canvas.height = Math.floor(this._screenElement.clientHeight * window.devicePixelRatio);
}
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
for (const decoration of this._decorationService.decorations) {
this._renderDecoration(decoration, updateAnchor);
}
}
private _renderDecoration(decoration: IInternalDecoration, updateAnchor?: boolean): void {
const element = this._decorationElements.get(decoration);
if (!element) {
this._decorationElements.set(decoration, this._canvas);
}
this._refreshStyle(decoration, updateAnchor);
decoration.onRenderEmitter.fire(this._canvas);
}
private _queueRefresh(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void {
if (this._animationFrame !== undefined) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => {
this._refreshDecorations(updateCanvasDimensions, updateAnchor);
this._animationFrame = undefined;
});
}
private _removeDecoration(decoration: IInternalDecoration): void {
this._decorationElements.get(decoration)?.remove();
this._decorationElements.delete(decoration);
}
}
+20 -7
View File
@@ -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 { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -71,6 +74,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _helperContainer: HTMLElement | undefined;
private _compositionView: HTMLElement | undefined;
private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
// private _visualBellTimer: number;
public browser: IBrowser = Browser as any;
@@ -78,6 +83,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
// browser services
private _decorationService: DecorationService;
private _charSizeService: ICharSizeService | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
@@ -109,7 +115,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 +164,8 @@ 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);
this._instantiationService.setService(IDecorationService, this._decorationService);
// Setup InputHandler listeners
this.register(this._inputHandler.onRequestBell(() => this.bell()));
@@ -471,6 +477,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._viewportElement = document.createElement('div');
this._viewportElement.classList.add('xterm-viewport');
fragment.appendChild(this._viewportElement);
this._viewportScrollArea = document.createElement('div');
this._viewportScrollArea.classList.add('xterm-scroll-area');
this._viewportElement.appendChild(this._viewportScrollArea);
@@ -576,8 +583,7 @@ 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.screenElement, this._renderService, this._bufferService);
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
// This event listener must be registered aftre MouseZoneManager is created
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
@@ -595,6 +601,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
}
if (this.options.overviewRulerWidth) {
this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement);
}
this.optionsService.onOptionChange(() => {
if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement);
}});
// Measure the character size
this._charSizeService.measure();
@@ -1005,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);
}
/**
+1
View File
@@ -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;
-8
View File
@@ -115,11 +115,3 @@ export interface ICharacterJoinerService {
deregister(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
export const IDecorationService = createDecorator<IDecorationService>('DecorationService');
export interface IDecorationService extends IDisposable {
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
refresh(): void;
attachToDom(screenElement: HTMLElement, renderService: IRenderService, bufferService: IBufferService): void;
}