diff --git a/demo/client.ts b/demo/client.ts index 7eba89a5..f6223f2c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -554,8 +554,10 @@ function addDecoration() { function addOverviewRuler() { const canvas = term.registerDecoration({marker: term.addMarker(1), overviewRulerItemColor: 'red'}); - canvas.element!.style.left = `${document.querySelector('.xterm-viewport').clientWidth + 5}px`; term.registerDecoration({marker: term.addMarker(3), overviewRulerItemColor: 'green'}); term.registerDecoration({marker: term.addMarker(5), overviewRulerItemColor: 'blue'}); + canvas.onRender((e) => { + e.style.left = `${document.querySelector('.xterm-viewport').clientWidth + 5}px`; + }); } diff --git a/src/browser/Decorations/OverviewRulerRenderer.ts b/src/browser/Decorations/OverviewRulerRenderer.ts index b880fa33..8a78bb78 100644 --- a/src/browser/Decorations/OverviewRulerRenderer.ts +++ b/src/browser/Decorations/OverviewRulerRenderer.ts @@ -5,36 +5,37 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IRenderService } from 'browser/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; -import { IBufferService, IDecorationService, IInstantiationService, IInternalDecoration } from 'common/services/Services'; -import { IDecorationOptions, IDecoration, IMarker } from 'xterm'; +import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services'; const enum ScrollbarConstants { - WIDTH = 7 + WIDTH = 15 } export class OverviewRulerRenderer extends Disposable { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D | null; - private _decorations: ScrollbarDecoration[] = []; - private _width: number | undefined; - private _anchor: 'right' | 'left' | undefined; - private _x: number | undefined; + private readonly _decorationElements: Map = new Map(); + + private _animationFrame: number | undefined; constructor( private readonly _viewportElement: HTMLElement, private readonly _screenElement: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @IDecorationService private readonly _decorationService: IDecorationService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IRenderService private readonly _renderService: IRenderService + @IRenderService private readonly _renderService: IRenderService, + @IOptionsService private readonly _optionsService: IOptionsService ) { 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._canvas.style.width = `${this._optionsService.options.overviewRulerWidth || ScrollbarConstants.WIDTH}px`; + this._canvas.style.height = `${this._screenElement.clientHeight}px`; + this._canvas.width = Math.floor((this._optionsService.options.overviewRulerWidth|| ScrollbarConstants.WIDTH)* window.devicePixelRatio); + this._canvas.height = Math.floor(this._screenElement.clientHeight * window.devicePixelRatio); this.refreshDecorations(); this.register(this._bufferService.buffers.onBufferActivate(() => { this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; @@ -42,111 +43,80 @@ export class OverviewRulerRenderer extends Disposable { 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.registerDecoration(e))); - this.register(this._decorationService.onDecorationRemoved(d => d.dispose())); + this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh())); + this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration))); } - public registerDecoration(decoration: IInternalDecoration): void { - if (!this._ctx || !decoration.options.overviewRulerItemColor) { + + 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): void { + if (!this._ctx) { + return; + } + 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.overviewRulerItemColor) { + this._decorationElements.delete(decoration); return; } - - // TODO: Does this do anything anymore? - this._instantiationService.createInstance(ScrollbarDecoration, { marker: decoration.options.marker, overviewRulerItemColor: decoration.options.overviewRulerItemColor }); - this._ctx.lineWidth = 1; this._ctx.strokeStyle = decoration.options.overviewRulerItemColor; this._ctx.strokeRect( 0, - Math.round(this._canvas.height * (decoration.marker.line / this._bufferService.buffers.active.lines.length)), + Math.round(this._canvas.height * (decoration.options.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.width = `${this._canvas.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.width = Math.floor((this._canvas.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); + + for (const decoration of this._decorationService.decorations) { + this._renderDecoration(decoration); } } - public override dispose(): void { - for (const decoration of this._decorations) { - decoration.dispose(); + + private _renderDecoration(decoration: IInternalDecoration): void { + const element = this._decorationElements.get(decoration); + if (!element) { + this._decorationElements.set(decoration, this._canvas); } - 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()); + this._refreshStyle(decoration); + decoration.onRenderEmitter.fire(this._canvas); } - 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) { + private _queueRefresh(): void { + if (this._animationFrame !== undefined) { 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(); + this._animationFrame = window.requestAnimationFrame(() => { + this.refreshDecorations(); + this._animationFrame = undefined; + }); + } + + private _removeDecoration(decoration: IInternalDecoration): void { + const element = this._decorationElements.get(decoration); + element?.remove(); + this._decorationElements.delete(decoration); } } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index b1f96ee5..6d0aabb1 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -75,7 +75,6 @@ export class Terminal extends CoreTerminal implements ITerminal { private _compositionView: HTMLElement | undefined; private _overviewRulerRenderer: OverviewRulerRenderer | undefined; - private _bufferDecorationRenderer: BufferDecorationRenderer | undefined; // private _visualBellTimer: number; @@ -587,16 +586,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); - if (this._decorationService) { - this._bufferDecorationRenderer = this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement); - } - if (this.options.overviewRulerWidth && this._decorationService) { - this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement); - } - this.optionsService.onOptionChange(() => { - if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._renderService && this._viewportElement && this.screenElement && this._decorationService) { - this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement); - }}); + 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))); @@ -614,6 +604,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(); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 43fe9981..4008a431 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -52,7 +52,8 @@ export const DEFAULT_OPTIONS: Readonly = { altClickMovesCursor: true, convertEol: false, termName: 'xterm', - cancelEvents: false + cancelEvents: false, + overviewRulerWidth: undefined }; const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 00f4072f..986e77c9 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -266,6 +266,11 @@ 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. + */ + overviewRulerWidth?: number; } /** @@ -394,7 +399,7 @@ declare module 'xterm' { } /** - * Represents a disposable with an + * Represents a disposable with an * @param onDispose event listener and * @param isDisposed property. */ @@ -436,7 +441,7 @@ declare module 'xterm' { /** * Options provided when registering a decoration - * containing a @param marker, @param anchor, + * containing a @param marker, @param anchor, * @param x offset from the anchor, @param width in cells * and @param height in cells. */ @@ -455,19 +460,19 @@ declare module 'xterm' { /** * The x position offset relative to the anchor - */ + */ x?: number; /** - * The width of the decoration in cells, which defaults to + * The width of the decoration in cells, which defaults to * cell width or the width in pixels, when an overlayRulerItemColor * is provided. */ width?: number; /** - * The height of the decoration in cells, which defaults to + * The height of the decoration in cells, which defaults to * cell height */ height?: number; @@ -946,7 +951,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.