Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
a5a9c3b
add scroll decorations
meganrogge Mar 8, 2022
4350c7f
get it to work
meganrogge Mar 8, 2022
3a499fc
clear on dispose
meganrogge Mar 9, 2022
785a819
clean up
meganrogge Mar 9, 2022
d46a685
more cleanup
meganrogge Mar 9, 2022
89112fb
refactor to return / store IDecorations
meganrogge Mar 9, 2022
249e73c
delete unused code
meganrogge Mar 9, 2022
1948ec8
only clear relevant part of canvas on marker dispose
meganrogge Mar 9, 2022
cfd7912
enable registering a decoration before attach to dom has happened
meganrogge Mar 9, 2022
e797070
call on render
meganrogge Mar 9, 2022
724bbeb
assign element in render call
meganrogge Mar 9, 2022
89a0cee
when alt buffer is active, hide decorations
meganrogge Mar 9, 2022
167a6fd
use position sticky
meganrogge Mar 9, 2022
bdfc82d
delete unused import
meganrogge Mar 10, 2022
2f85448
Update css/xterm.css
meganrogge Mar 11, 2022
98a03dd
re-arrange dom structure
meganrogge Mar 11, 2022
a14248d
Merge branch 'merogge/scroll-bar-decorations' of https://github.com/m…
meganrogge Mar 11, 2022
1250ab6
insert before
meganrogge Mar 11, 2022
4022277
scroll -> overviewRuler
meganrogge Mar 12, 2022
692df15
part 1 of massive refactor
meganrogge Mar 12, 2022
9e598d9
allow setting width
meganrogge Mar 14, 2022
3b279e4
round to the nearest pixel
meganrogge Mar 14, 2022
ced0662
large refactor, broken
meganrogge Mar 14, 2022
7dc3332
commit before reverting
meganrogge Mar 14, 2022
c8d1266
Fix buffer decoration onRender, start using internal decoration
Tyriar Mar 14, 2022
4b615f4
Reduce state in buffer decorations
Tyriar Mar 14, 2022
65b9a6c
Fix dependency injection for decoration service
Tyriar Mar 14, 2022
3dc4f1a
Remove old decoration elements
Tyriar Mar 14, 2022
fc09730
Tidy up DecorationService
Tyriar Mar 14, 2022
bfb0481
register service
meganrogge Mar 15, 2022
079bbc8
get it working in demo with overviewRulerwidth not considered"
meganrogge Mar 15, 2022
3e47c96
get demo to work the right way
meganrogge Mar 15, 2022
39f9c35
rename css, fix background color getting applied
meganrogge Mar 15, 2022
4ef5f6a
add listener for on option change
meganrogge Mar 15, 2022
520d5dd
delete unused css
meganrogge Mar 15, 2022
f98b4ce
properly dispose of buffer decoration
meganrogge Mar 15, 2022
80e7963
fix tests and add one
meganrogge Mar 15, 2022
e7e9d5c
remove test
meganrogge Mar 15, 2022
df929f2
add position
meganrogge Mar 15, 2022
bd21c6a
adjust test
meganrogge Mar 15, 2022
71649f5
fix tests
meganrogge Mar 15, 2022
adaee5e
use poll for instead
meganrogge Mar 15, 2022
1905d7d
add overview ruler tests
meganrogge Mar 15, 2022
21263a5
cleanup
meganrogge Mar 15, 2022
a93f81c
Update typings/xterm.d.ts
meganrogge Mar 15, 2022
8488f1a
Update src/browser/Decorations/OverviewRulerRenderer.ts
meganrogge Mar 15, 2022
5b87772
Update typings/xterm.d.ts
meganrogge Mar 15, 2022
45ecc83
Update typings/xterm.d.ts
meganrogge Mar 15, 2022
b146ffd
Update src/browser/Decorations/OverviewRulerRenderer.ts
meganrogge Mar 15, 2022
fdc2bc4
Update typings/xterm.d.ts
meganrogge Mar 15, 2022
cee2411
more cleanup
meganrogge Mar 15, 2022
1c38f85
Merge branch 'merogge/scroll-bar-decorations' of https://github.com/m…
meganrogge Mar 15, 2022
4cc0db4
Update src/browser/Decorations/OverviewRulerRenderer.ts
meganrogge Mar 15, 2022
e7baccd
Update src/browser/Terminal.ts
meganrogge Mar 15, 2022
9f6d4f5
Merge branch 'master' into merogge/scroll-bar-decorations
meganrogge Mar 15, 2022
a9a6920
more cleanup
meganrogge Mar 15, 2022
19b212d
Merge branch 'merogge/scroll-bar-decorations' of https://github.com/m…
meganrogge Mar 15, 2022
8ccbef8
only update canvas dimensions when needed
meganrogge Mar 15, 2022
53c432d
add condition for when to update anchor
meganrogge Mar 15, 2022
b05db37
adjust size based on position
meganrogge Mar 15, 2022
d92e636
Update src/browser/Terminal.ts
meganrogge Mar 15, 2022
035a7f7
Update src/browser/Terminal.ts
meganrogge Mar 15, 2022
5777fbc
Update src/browser/tsconfig.json
meganrogge Mar 15, 2022
6f5008d
Update typings/xterm.d.ts
meganrogge Mar 15, 2022
a4f830a
Update typings/xterm.d.ts
meganrogge Mar 15, 2022
22d889a
use this._width instead of options
meganrogge Mar 15, 2022
963ed59
tweak demo
meganrogge Mar 15, 2022
7f85922
Merge branch 'merogge/scroll-bar-decorations' of https://github.com/m…
meganrogge Mar 15, 2022
c277fb7
add overviewRuler to addDecoration
meganrogge Mar 16, 2022
a0a7abb
add overview options to decoration in demo
meganrogge Mar 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions css/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,16 @@
z-index: 6;
position: absolute;
}

.xterm-decoration-scrollbar {
z-index: 7;
position: sticky;
top: 0px;
right: 0px;
width: 50px;
}

.xterm-decoration-scrollbar.demo-scrollbar {
height: 436px;
z-index:10;
}
10 changes: 10 additions & 0 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -550,3 +551,12 @@ function addDecoration() {
decoration.element.style.backgroundColor = 'red';
});
}

function addScrollbarDecoration() {
document.querySelector('.xterm-decoration-scrollbar')?.classList.add('demo-scrollbar');
const scrollbarDecorationCanvas = term.registerDecoration({marker: term.addMarker(1), scrollbarDecorationColor: 'red'});
scrollbarDecorationCanvas.element!.style.left = `${scrollbarDecorationCanvas.element!.nextElementSibling!.clientWidth + 89}px`;
term.registerDecoration({marker: term.addMarker(3), scrollbarDecorationColor: 'green'});
term.registerDecoration({marker: term.addMarker(5), scrollbarDecorationColor: 'blue'});
}

1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ <h3>Test</h3>
<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-scrollbar-decoration" title="Add a scrollbar decoration to the terminal">Add Scrollbar Decoration</button>
</div>
</div>
</div>
Expand Down
9 changes: 8 additions & 1 deletion src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _viewportElement: HTMLElement | undefined;
private _helperContainer: HTMLElement | undefined;
private _compositionView: HTMLElement | undefined;
private _scrollbarDecorationNode: HTMLCanvasElement | undefined;

// private _visualBellTimer: number;

Expand Down Expand Up @@ -471,6 +472,12 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._viewportElement = document.createElement('div');
this._viewportElement.classList.add('xterm-viewport');
fragment.appendChild(this._viewportElement);

// TODO: make this opt in, must be done before the scroll area in order to show up
this._scrollbarDecorationNode = document.createElement('canvas');
this._scrollbarDecorationNode.classList.add('xterm-decoration-scrollbar');
this._viewportElement?.appendChild(this._scrollbarDecorationNode);

this._viewportScrollArea = document.createElement('div');
this._viewportScrollArea.classList.add('xterm-scroll-area');
this._viewportElement.appendChild(this._viewportScrollArea);
Expand Down Expand Up @@ -577,7 +584,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
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.decorationService.attachToDom(this._renderService, this.screenElement, this._viewportElement, this._scrollbarDecorationNode);
// This event listener must be registered aftre MouseZoneManager is created
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));

Expand Down
191 changes: 161 additions & 30 deletions src/browser/services/DecorationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,198 @@
* @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
}

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(); }
private _screenElement: HTMLElement | undefined;
private _viewportElement: HTMLElement | undefined;
private _bufferDecorationContainer: HTMLElement | undefined;
private _scrollbarDecorationCanvas: CanvasRenderingContext2D | null = null;
private _scrollbarDecorationNode: HTMLCanvasElement | undefined;

private readonly _bufferDecorations: BufferDecoration[] = [];
private _scrollbarDecorations: ScrollbarDecoration[] = [];

public attachToDom(screenElement: HTMLElement, renderService: IRenderService): void {
constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService, @IBufferService private readonly _bufferService: IBufferService) { super(); }

public attachToDom(renderService: IRenderService, screenElement: HTMLElement, viewportElement: HTMLElement, scrollbarDecorationNode: HTMLCanvasElement): 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)));
this._viewportElement = viewportElement;
this._scrollbarDecorationNode = scrollbarDecorationNode;

this.register(this._renderService.onRenderedBufferChange(() => this._refresh()));
this.register(this._renderService.onDimensionsChange(() => this._refresh(true)));
this.register(addDisposableDomListener(window, 'resize', () => this._refreshScollbarDecorations()));
this.register(this._bufferService.buffers.onBufferActivate(() => {
this._scrollbarDecorationNode!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
}));
}

public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
if (decorationOptions.marker.isDisposed || !this._container) {
if (decorationOptions.marker.isDisposed) {
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;
if (decorationOptions.scrollbarDecorationColor) {
return this._registerScrollbarDecoration(decorationOptions.marker, decorationOptions.scrollbarDecorationColor);
}
return this._registerBufferDecoration(decorationOptions);
}

public dispose(): void {
for (const bufferDecoration of this._bufferDecorations) {
bufferDecoration.dispose();
}
for (const scrollbarDecoration of this._scrollbarDecorations) {
scrollbarDecoration.dispose();
}
if (this._screenElement && this._bufferDecorationContainer && this._screenElement.contains(this._bufferDecorationContainer)) {
this._screenElement.removeChild(this._bufferDecorationContainer);
}
this._scrollbarDecorations = [];
this._scrollbarDecorationNode?.remove();
}

private _refresh(shouldRecreate?: boolean): void {
this._refreshBufferDecorations(shouldRecreate);
this._refreshScollbarDecorations();
}

private _queueRefresh(): void {
if (this._animationFrame !== undefined) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => {
this.refresh();
this._refresh();
this._animationFrame = undefined;
});
}

public refresh(shouldRecreate?: boolean): void {
private _registerBufferDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
if (this._screenElement && !this._bufferDecorationContainer) {
this._bufferDecorationContainer = document.createElement('div');
this._bufferDecorationContainer.classList.add('xterm-decoration-container');
this._screenElement.appendChild(this._bufferDecorationContainer);
}
const decoration = new BufferDecoration(this._bufferService, decorationOptions, this._bufferDecorationContainer);
this._bufferDecorations.push(decoration);
decoration.onDispose(() => this._bufferDecorations.splice(this._bufferDecorations.indexOf(decoration), 1));
this._queueRefresh();
return decoration;
}

private _registerScrollbarDecoration(marker: IMarker, color: string): IDecoration | undefined {
if (!this._scrollbarDecorationNode || !this._viewportElement) {
return;
}
if (!this._scrollbarDecorationCanvas) {
this._scrollbarDecorationCanvas = this._scrollbarDecorationNode.getContext('2d');
this._refreshScollbarDecorations();
}
const decoration = this._instantiationService.createInstance(ScrollbarDecoration, { marker, scrollbarDecorationColor: color }, this._scrollbarDecorationNode, this._scrollbarDecorationCanvas!);
decoration.onDispose(() => this._scrollbarDecorations.splice(this._scrollbarDecorations.indexOf(decoration), 1));
this._scrollbarDecorations.push(decoration);
return decoration;
}

private _refreshBufferDecorations(shouldRecreate?: boolean): void {
if (!this._renderService) {
return;
}
for (const decoration of this._decorations) {
for (const decoration of this._bufferDecorations) {
decoration.render(this._renderService, shouldRecreate);
}
}

public dispose(): void {
for (const decoration of this._decorations) {
decoration.dispose();
private _refreshScollbarDecorations(): void {
if (!this._scrollbarDecorationCanvas || !this._viewportElement || !this._scrollbarDecorationNode) {
return;
}
this._scrollbarDecorationNode.style.width = `${ScrollbarConstants.WIDTH}px`;
this._scrollbarDecorationNode.style.height = `${this._viewportElement.clientHeight}px`;
this._scrollbarDecorationNode.width = Math.floor(ScrollbarConstants.WIDTH * window.devicePixelRatio);
this._scrollbarDecorationNode.height = Math.floor(this._viewportElement.clientHeight * window.devicePixelRatio);
this._scrollbarDecorationCanvas.clearRect(0, 0, this._scrollbarDecorationCanvas.canvas.width, this._scrollbarDecorationCanvas.canvas.height);
for (const decoration of this._scrollbarDecorations) {
decoration.render();
}
}
}

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 { return this._element!; }
public get marker(): IMarker { return this._marker; }
public get color(): string { return this._color!; }

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; }

constructor(
options: IDecorationOptions,
private readonly _canvas: HTMLCanvasElement,
private readonly _ctx: CanvasRenderingContext2D,
@IBufferService private readonly _bufferService: IBufferService
) {
super();
this._marker = options.marker;
this._color = options.scrollbarDecorationColor;
this._marker.onDispose(() => this.dispose());
this.render();
}
public render(): void {
if (!this._element) {
this._element = this._canvas;
}
if (this._screenElement && this._container && this._screenElement.contains(this._container)) {
this._screenElement.removeChild(this._container);
this._ctx.lineWidth = 1;
this._ctx.strokeStyle = this.color;
this._ctx.strokeRect(
0,
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) {
return;
}
this._ctx.clearRect(
0,
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 Decoration extends Disposable implements IDecoration {

export class BufferDecoration extends Disposable implements IDecoration {
private readonly _marker: IMarker;
private _element: HTMLElement | undefined;

Expand All @@ -83,15 +209,17 @@ export class Decoration extends Disposable implements IDecoration {
private _onRender = new EventEmitter<HTMLElement>();
public get onRender(): IEvent<HTMLElement> { 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,
@IBufferService private readonly _bufferService: IBufferService
private readonly _container?: HTMLElement
) {
super();
this.x = options.x ?? 0;
Expand All @@ -100,6 +228,9 @@ export class Decoration extends Disposable implements IDecoration {
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 {
Expand All @@ -116,7 +247,7 @@ export class Decoration extends Disposable implements IDecoration {
}

private _createElement(renderService: IRenderService, shouldRecreate?: boolean): void {
if (shouldRecreate && this._element && this._container.contains(this._element)) {
if (shouldRecreate && this._element && this._container && this._container.contains(this._element)) {
this._container.removeChild(this._element);
}
this._element = document.createElement('div');
Expand Down Expand Up @@ -147,12 +278,12 @@ export class Decoration extends Disposable implements IDecoration {
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';
this._element.style.display = this._altBufferIsActive ? 'none' : 'block';
}
}

public override dispose(): void {
if (this.isDisposed) {
if (this.isDisposed || !this._container) {
return;
}
if (this._element && this._container.contains(this._element)) {
Expand Down
3 changes: 1 addition & 2 deletions src/browser/services/Services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ export interface ICharacterJoinerService {

export const IDecorationService = createDecorator<IDecorationService>('DecorationService');
export interface IDecorationService extends IDisposable {
attachToDom(renderService: IRenderService, screenElement: HTMLElement, viewportElement: HTMLElement, scrollbarDecorationNode: HTMLCanvasElement): void;
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
refresh(): void;
attachToDom(screenElement: HTMLElement, renderService: IRenderService, bufferService: IBufferService): void;
}
10 changes: 8 additions & 2 deletions typings/xterm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,8 @@ declare module 'xterm' {
readonly onRender: IEvent<HTMLElement>;

/**
* The HTMLElement that gets created after the
* first _onRender call, or undefined if accessed before
* The HTMLElement that gets created or drawn to (for scrollbar decorations)
* after the first _onRender call, or undefined if accessed before
* that.
*/
readonly element: HTMLElement | undefined;
Expand Down Expand Up @@ -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;
}

/**
Expand Down