diff --git a/css/xterm.css b/css/xterm.css index ab3965b4..3ae486c4 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -178,3 +178,17 @@ z-index: 6; position: absolute; } + +.xterm-decoration-scrollbar { + z-index: 7; + position: fixed; + top: 0px; + right: 0px; + width: 50px; +} + +.xterm-decoration-scrollbar.demo-scrollbar { + height: 436px; + left: 872px; + top: 83px; +} diff --git a/demo/client.ts b/demo/client.ts index aee23401..ec7e95ee 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -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-scrollbar-decoration').addEventListener('click', addScrollbarDecoration); } function createTerminal(): void { @@ -550,3 +551,11 @@ function addDecoration() { decoration.element.style.backgroundColor = 'red'; }); } + +function addScrollbarDecoration() { + term.registerDecoration({marker: term.addMarker(1), scrollbarDecorationColor: 'red'}); + term.registerDecoration({marker: term.addMarker(3), scrollbarDecorationColor: 'green'}); + term.registerDecoration({marker: term.addMarker(5), scrollbarDecorationColor: 'blue'}); + document.querySelector('.xterm-decoration-scrollbar').classList.add('demo-scrollbar'); +} + diff --git a/demo/index.html b/demo/index.html index e024222c..dab52eec 100644 --- a/demo/index.html +++ b/demo/index.html @@ -69,6 +69,7 @@ + diff --git a/src/browser/services/DecorationService.ts b/src/browser/services/DecorationService.ts index ed3c7224..dfbcd918 100644 --- a/src/browser/services/DecorationService.ts +++ b/src/browser/services/DecorationService.ts @@ -3,6 +3,7 @@ * @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'; @@ -17,7 +18,11 @@ export class DecorationService extends Disposable implements IDecorationService private _renderService: IRenderService | undefined; private _animationFrame: number | undefined; - constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { super(); } + private _scrollbarDecorationCanvas: CanvasRenderingContext2D | null = null; + private _scrollbarDecorationNode: HTMLCanvasElement | undefined; + private _scrollbarDecorations: { marker: IMarker, color?: string}[] = []; + + constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService, @IBufferService private readonly _bufferService: IBufferService) { super(); } public attachToDom(screenElement: HTMLElement, renderService: IRenderService): void { this._renderService = renderService; @@ -27,12 +32,22 @@ export class DecorationService extends Disposable implements IDecorationService screenElement.appendChild(this._container); this.register(this._renderService.onRenderedBufferChange(() => this.refresh())); this.register(this._renderService.onDimensionsChange(() => this.refresh(true))); + this.register(addDisposableDomListener(window, 'resize', () => this._refreshScollbarDecorations())); } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { if (decorationOptions.marker.isDisposed || !this._container) { return undefined; } + if (decorationOptions.scrollbarDecorationColor) { + if (!this._scrollbarDecorationCanvas) { + this._scrollbarDecorationNode = document.createElement('canvas'); + this._scrollbarDecorationNode.classList.add('xterm-decoration-scrollbar'); + this._screenElement?.parentElement?.appendChild(this._scrollbarDecorationNode); + this._scrollbarDecorationCanvas = this._scrollbarDecorationNode.getContext('2d'); + } + this._registerScrollbarDecoration(decorationOptions.marker, decorationOptions.scrollbarDecorationColor); + } const decoration = this._instantiationService.createInstance(Decoration, decorationOptions, this._container); this._decorations.push(decoration); decoration.onDispose(() => this._decorations.splice(this._decorations.indexOf(decoration), 1)); @@ -57,6 +72,7 @@ export class DecorationService extends Disposable implements IDecorationService for (const decoration of this._decorations) { decoration.render(this._renderService, shouldRecreate); } + this._refreshScollbarDecorations(); } public dispose(): void { @@ -67,6 +83,41 @@ export class DecorationService extends Disposable implements IDecorationService this._screenElement.removeChild(this._container); } } + + private _registerScrollbarDecoration(marker: IMarker, color?: string): HTMLCanvasElement | undefined { + this._scrollbarDecorations.push({ marker, color }); + if (!this._scrollbarDecorationCanvas) { + return; + } + this._addScrollbarDecoration(marker, color); + } + + private _refreshScollbarDecorations(): void { + if (!this._scrollbarDecorationCanvas) { + return; + } + if (this._scrollbarDecorationNode) { + this._scrollbarDecorationNode.style.width = '7px'; + this._scrollbarDecorationNode.style.height = `${this._screenElement?.parentElement!.clientHeight}px`; + this._scrollbarDecorationNode.width = Math.floor(7*window.devicePixelRatio); + this._scrollbarDecorationNode.height = Math.floor(436*window.devicePixelRatio); + } + this._scrollbarDecorationCanvas.clearRect(0, 0, this._scrollbarDecorationCanvas.canvas.width, this._scrollbarDecorationCanvas.canvas.height); + for (const scrollbarDecoration of this._scrollbarDecorations) { + this._addScrollbarDecoration(scrollbarDecoration.marker, scrollbarDecoration.color); + } + } + + private _addScrollbarDecoration(marker: IMarker, color?: string): void { + if (!this._scrollbarDecorationCanvas || !this._screenElement?.parentElement?.clientHeight) { + return; + } + this._scrollbarDecorationCanvas.lineWidth = 1; + if (color) { + this._scrollbarDecorationCanvas.strokeStyle = color; + } + this._scrollbarDecorationCanvas.strokeRect(0, (marker.line / (this._bufferService.buffers.active.lines.length) * Math.floor(436*window.devicePixelRatio)), Math.floor(7*window.devicePixelRatio), window.devicePixelRatio); + } } export class Decoration extends Disposable implements IDecoration { private readonly _marker: IMarker; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2cd4daa6..eb31c4a0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -470,6 +470,12 @@ declare module 'xterm' { * cell height */ height?: number; + + /** + * When provided, renders the decoration in the scrollbar + * with the given color + */ + scrollbarDecorationColor?: string; } /**