mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #3676 from meganrogge/merogge/scroll-bar-decorations
add overview ruler
This commit is contained in:
@@ -178,3 +178,10 @@
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 7;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
+18
-3
@@ -151,6 +151,7 @@ if (document.location.pathname === '/test') {
|
||||
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
|
||||
document.getElementById('load-test').addEventListener('click', loadTest);
|
||||
document.getElementById('add-decoration').addEventListener('click', addDecoration);
|
||||
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
|
||||
}
|
||||
|
||||
function createTerminal(): void {
|
||||
@@ -544,9 +545,23 @@ function loadTest() {
|
||||
}
|
||||
|
||||
function addDecoration() {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
const marker = term.addMarker(1);
|
||||
const decoration = term.registerDecoration({ marker });
|
||||
decoration.onRender(() => {
|
||||
decoration.element.style.backgroundColor = 'red';
|
||||
const decoration = term.registerDecoration({ marker, overviewRulerOptions: { color: '#ef2929'} });
|
||||
decoration.onRender((e) => {
|
||||
if (e.classList.value === 'xterm-decoration') {
|
||||
e.style.backgroundColor = '#ef2929';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addOverviewRuler() {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
term.registerDecoration({marker: term.addMarker(1), overviewRulerOptions: { color: '#ef2929' }});
|
||||
term.registerDecoration({marker: term.addMarker(3), overviewRulerOptions: { color: '#8ae234' }});
|
||||
term.registerDecoration({marker: term.addMarker(5), overviewRulerOptions: { color: '#729fcf' }});
|
||||
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }});
|
||||
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
|
||||
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
|
||||
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
|
||||
<button id="add-decoration" title="Add a decoration to the terminal">Decoration</button>
|
||||
<button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,141 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}));
|
||||
console.log(this._width/3);
|
||||
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
@@ -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();
|
||||
|
||||
@@ -1003,7 +1016,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
|
||||
return this.decorationService!.registerDecoration(decorationOptions);
|
||||
return this._decorationService.registerDecoration(decorationOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+1
@@ -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;
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
export class DecorationService extends Disposable implements IDecorationService {
|
||||
|
||||
private readonly _decorations: Decoration[] = [];
|
||||
private _container: HTMLElement | undefined;
|
||||
private _screenElement: HTMLElement | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _animationFrame: number | undefined;
|
||||
|
||||
constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { super(); }
|
||||
|
||||
public attachToDom(screenElement: HTMLElement, renderService: IRenderService): void {
|
||||
this._renderService = renderService;
|
||||
this._screenElement = screenElement;
|
||||
this._container = document.createElement('div');
|
||||
this._container.classList.add('xterm-decoration-container');
|
||||
screenElement.appendChild(this._container);
|
||||
this.register(this._renderService.onRenderedBufferChange(() => this.refresh()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this.refresh(true)));
|
||||
}
|
||||
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
|
||||
if (decorationOptions.marker.isDisposed || !this._container) {
|
||||
return undefined;
|
||||
}
|
||||
const decoration = this._instantiationService.createInstance(Decoration, decorationOptions, this._container);
|
||||
this._decorations.push(decoration);
|
||||
decoration.onDispose(() => this._decorations.splice(this._decorations.indexOf(decoration), 1));
|
||||
this._queueRefresh();
|
||||
return decoration;
|
||||
}
|
||||
|
||||
private _queueRefresh(): void {
|
||||
if (this._animationFrame !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this.refresh();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public refresh(shouldRecreate?: boolean): void {
|
||||
if (!this._renderService) {
|
||||
return;
|
||||
}
|
||||
for (const decoration of this._decorations) {
|
||||
decoration.render(this._renderService, shouldRecreate);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const decoration of this._decorations) {
|
||||
decoration.dispose();
|
||||
}
|
||||
if (this._screenElement && this._container && this._screenElement.contains(this._container)) {
|
||||
this._screenElement.removeChild(this._container);
|
||||
}
|
||||
}
|
||||
}
|
||||
export class Decoration 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<void>();
|
||||
public get onDispose(): IEvent<void> { return this._onDispose.event; }
|
||||
|
||||
private _onRender = new EventEmitter<HTMLElement>();
|
||||
public get onRender(): IEvent<HTMLElement> { return this._onRender.event; }
|
||||
|
||||
public x: number;
|
||||
public anchor: 'left' | 'right';
|
||||
public width: number;
|
||||
public height: number;
|
||||
|
||||
constructor(
|
||||
options: IDecorationOptions,
|
||||
private readonly _container: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
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(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.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._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
if (this._element && this._container.contains(this._element)) {
|
||||
this._container.removeChild(this._element);
|
||||
}
|
||||
this.isDisposed = true;
|
||||
this._onDispose.fire();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IDecorationService, IInternalDecoration } from 'common/services/Services';
|
||||
import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm';
|
||||
|
||||
export class DecorationService extends Disposable implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
|
||||
private readonly _decorations: IInternalDecoration[] = [];
|
||||
|
||||
private _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
|
||||
public get onDecorationRegistered(): IEvent<IInternalDecoration> { return this._onDecorationRegistered.event; }
|
||||
private _onDecorationRemoved = this.register(new EventEmitter<IInternalDecoration>());
|
||||
public get onDecorationRemoved(): IEvent<IInternalDecoration> { return this._onDecorationRemoved.event; }
|
||||
|
||||
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
|
||||
|
||||
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(decoration);
|
||||
}
|
||||
return decoration;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const decoration of this._decorations) {
|
||||
this._onDecorationRemoved.fire(decoration);
|
||||
decoration.dispose();
|
||||
}
|
||||
this._decorations.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class Decoration extends Disposable implements IInternalDecoration {
|
||||
public readonly marker: IMarker;
|
||||
public element: HTMLElement | undefined;
|
||||
public isDisposed: boolean = false;
|
||||
|
||||
public readonly onRenderEmitter = this.register(new EventEmitter<HTMLElement>());
|
||||
public readonly onRender = this.onRenderEmitter.event;
|
||||
private _onDispose = this.register(new EventEmitter<void>());
|
||||
public readonly onDispose = this._onDispose.event;
|
||||
|
||||
constructor(
|
||||
public readonly options: IDecorationOptions
|
||||
) {
|
||||
super();
|
||||
this.marker = options.marker;
|
||||
}
|
||||
public override dispose(): void {
|
||||
this._onDispose.fire();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,8 @@ export const DEFAULT_OPTIONS: Readonly<ITerminalOptions> = {
|
||||
altClickMovesCursor: true,
|
||||
convertEol: false,
|
||||
termName: 'xterm',
|
||||
cancelEvents: false
|
||||
cancelEvents: false,
|
||||
overviewRulerWidth: undefined
|
||||
};
|
||||
|
||||
const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IEvent, IEventEmitter } 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<IBufferService>('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,16 @@ export interface IUnicodeVersionProvider {
|
||||
readonly version: string;
|
||||
wcwidth(ucs: number): 0 | 1 | 2;
|
||||
}
|
||||
|
||||
export const IDecorationService = createDecorator<IDecorationService>('DecorationService');
|
||||
export interface IDecorationService extends IDisposable {
|
||||
serviceBrand: undefined;
|
||||
readonly decorations: IterableIterator<IInternalDecoration>;
|
||||
readonly onDecorationRegistered: IEvent<IInternalDecoration>;
|
||||
readonly onDecorationRemoved: IEvent<IInternalDecoration>;
|
||||
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
|
||||
}
|
||||
export interface IInternalDecoration extends IDecoration {
|
||||
readonly options: IDecorationOptions;
|
||||
readonly onRenderEmitter: IEventEmitter<HTMLElement>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"declarationMap": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true
|
||||
}
|
||||
}
|
||||
|
||||
+54
-42
@@ -574,7 +574,7 @@ describe('API Integration Tests', function(): void {
|
||||
await page.evaluate(`window.term.scrollLines(10)`);
|
||||
await page.evaluate(`window.term.addMarker(3)`);
|
||||
await page.evaluate(`window.term.addMarker(4)`);
|
||||
await page.evaluate(`
|
||||
await page.evaluate(`
|
||||
for (let i = 0; i < window.term.markers.length; ++i) {
|
||||
const marker = window.term.markers[i];
|
||||
marker.onDispose(() => window.disposeStack.push(marker));
|
||||
@@ -732,48 +732,60 @@ describe('API Integration Tests', function(): void {
|
||||
});
|
||||
|
||||
describe('registerDecoration', () => {
|
||||
it('should register decorations and render them', async () => {
|
||||
await openTerminal(page);
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await page.evaluate(`window.marker1 = window.term.addMarker(1)`);
|
||||
await page.evaluate(`window.marker2 = window.term.addMarker(2)`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`);
|
||||
await page.evaluate(`window.term.resize(10, 5)`);
|
||||
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm-screen .xterm-decoration').length`), 2);
|
||||
describe('bufferDecorations', () => {
|
||||
it('should register decorations and render them when terminal open is called', async () => {
|
||||
await page.evaluate(`window.term = new Terminal({})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
await page.evaluate(`window.marker1 = window.term.addMarker(1)`);
|
||||
await page.evaluate(`window.marker2 = window.term.addMarker(2)`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`);
|
||||
await openTerminal(page);
|
||||
await pollFor(page, `document.querySelectorAll('.xterm-screen .xterm-decoration').length`, 2);
|
||||
});
|
||||
it('should return undefined when the marker has already been disposed of', async () => {
|
||||
await openTerminal(page);
|
||||
await page.evaluate(`window.marker = window.term.addMarker(1)`);
|
||||
await page.evaluate(`window.marker.dispose()`);
|
||||
await pollFor(page, `window.decoration = window.term.registerDecoration({ marker: window.marker });`, undefined);
|
||||
});
|
||||
it('should throw when a negative x offset is provided', async () => {
|
||||
await openTerminal(page);
|
||||
await page.evaluate(`window.marker = window.term.addMarker(1)`);
|
||||
await page.evaluate(`
|
||||
try {
|
||||
window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 });
|
||||
} catch (e) {
|
||||
window.throwMessage = e.message;
|
||||
}
|
||||
`);
|
||||
await pollFor(page, 'window.throwMessage', 'This API only accepts positive integers');
|
||||
});
|
||||
});
|
||||
it('on resize should dispose of the old decoration and create a new one', async () => {
|
||||
await openTerminal(page);
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await page.evaluate(`window.marker = window.term.addMarker(1)`);
|
||||
await page.evaluate(`window.decoration = window.term.registerDecoration({ marker: window.marker })`);
|
||||
await page.evaluate(`window.term.resize(10, 5)`);
|
||||
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm-screen .xterm-decoration').length`), 1);
|
||||
});
|
||||
it('should return undefined when the marker has already been disposed of', async () => {
|
||||
await openTerminal(page);
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await page.evaluate(`window.marker = window.term.addMarker(1)`);
|
||||
await page.evaluate(`window.marker.dispose()`);
|
||||
assert.equal(await page.evaluate(`window.decoration = window.term.registerDecoration({ marker: window.marker });`), undefined);
|
||||
});
|
||||
it('should throw when a negative x offset is provided', async () => {
|
||||
await openTerminal(page);
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await writeSync(page, '\\n\\n\\n\\n');
|
||||
await page.evaluate(`window.marker = window.term.addMarker(1)`);
|
||||
await page.evaluate(`
|
||||
try {
|
||||
window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 });
|
||||
} catch (e) {
|
||||
window.throwMessage = e.message;
|
||||
}
|
||||
`);
|
||||
await pollFor(page, 'window.throwMessage', 'This API only accepts positive integers');
|
||||
describe('overviewRulerDecorations', () => {
|
||||
it('should not add an overview ruler when width is not set', async () => {
|
||||
await page.evaluate(`window.term = new Terminal({})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
await page.evaluate(`window.marker1 = window.term.addMarker(1)`);
|
||||
await page.evaluate(`window.marker2 = window.term.addMarker(2)`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red' } })`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue' } })`);
|
||||
await openTerminal(page);
|
||||
await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0);
|
||||
});
|
||||
it('should add an overview ruler when width is set', async () => {
|
||||
await page.evaluate(`window.term = new Terminal({ overviewRulerWidth: 15 })`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
await page.evaluate(`window.marker1 = window.term.addMarker(1)`);
|
||||
await page.evaluate(`window.marker2 = window.term.addMarker(2)`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red' } })`);
|
||||
await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue' } })`);
|
||||
await openTerminal(page);
|
||||
await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Vendored
+30
-20
@@ -266,6 +266,12 @@ declare module 'xterm' {
|
||||
* All features are disabled by default for security reasons.
|
||||
*/
|
||||
windowOptions?: IWindowOptions;
|
||||
|
||||
/**
|
||||
* The width, in pixels, of the canvas for the overview ruler. The overview
|
||||
* ruler will be hidden when not set.
|
||||
*/
|
||||
overviewRulerWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,7 +400,7 @@ declare module 'xterm' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a disposable with an
|
||||
* Represents a disposable that tracks is disposed state.
|
||||
* @param onDispose event listener and
|
||||
* @param isDisposed property.
|
||||
*/
|
||||
@@ -427,49 +433,53 @@ declare module 'xterm' {
|
||||
readonly onRender: IEvent<HTMLElement>;
|
||||
|
||||
/**
|
||||
* The HTMLElement that gets created after the
|
||||
* first _onRender call, or undefined if accessed before
|
||||
* The element that the decoration is rendered to. This will be undefined
|
||||
* until it is rendered for the first time by @{link IDecoration.onRender}.
|
||||
* that.
|
||||
*/
|
||||
readonly element: HTMLElement | undefined;
|
||||
element: HTMLElement | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options provided when registering a decoration
|
||||
* containing a @param marker, @param anchor,
|
||||
* @param x offset from the anchor, @param width in cells
|
||||
* and @param height in cells.
|
||||
/*
|
||||
* Options that define the presentation of the decoration.
|
||||
*/
|
||||
export interface IDecorationOptions {
|
||||
/**
|
||||
* The line in the terminal where
|
||||
* the decoration will be displayed
|
||||
*/
|
||||
marker: IMarker;
|
||||
readonly marker: IMarker;
|
||||
|
||||
/*
|
||||
* Where the decoration will be anchored -
|
||||
* defaults to the left edge
|
||||
*/
|
||||
anchor?: 'right' | 'left';
|
||||
readonly anchor?: 'right' | 'left';
|
||||
|
||||
/**
|
||||
* The x position offset relative to the anchor
|
||||
*/
|
||||
x?: number;
|
||||
*/
|
||||
readonly x?: number;
|
||||
|
||||
|
||||
/**
|
||||
* The width of the decoration in cells, which defaults to
|
||||
* cell width
|
||||
* The width of the decoration in cells, defaults to 1.
|
||||
*/
|
||||
width?: number;
|
||||
readonly width?: number;
|
||||
|
||||
/**
|
||||
* The height of the decoration in cells, which defaults to
|
||||
* cell height
|
||||
* The height of the decoration in cells, defaults to 1.
|
||||
*/
|
||||
height?: number;
|
||||
readonly height?: number;
|
||||
|
||||
/**
|
||||
* When defined, renders the decoration in the overview ruler to the right
|
||||
* of the terminal. {@link ITerminalOptions.overviewRulerWidth} must be set
|
||||
* in order to see the overview ruler.
|
||||
* @param color The color of the decoration.
|
||||
* @param position The position of the decoration.
|
||||
*/
|
||||
readonly overviewRulerOptions?: { color: string; position?: 'left' | 'center' | 'right'}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -939,7 +949,7 @@ declare module 'xterm' {
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Adds a decoration to the terminal using
|
||||
* @param decorationOptions, which takes a marker and an optional anchor,
|
||||
* @param decorationOptions, which takes a marker and an optional anchor,
|
||||
* width, height, and x offset from the anchor. Returns the decoration or
|
||||
* undefined if the alt buffer is active or the marker has already been disposed of.
|
||||
* @throws when options include a negative x offset.
|
||||
|
||||
Reference in New Issue
Block a user