Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial decorations API #3302

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
4 changes: 4 additions & 0 deletions addons/xterm-addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
}

public onDecorationsChanged(): void {
// TBD
}

public onCursorMove(): void {
for (const l of this._renderLayers) {
l.onCursorMove(this._terminal);
Expand Down
16 changes: 16 additions & 0 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ let protocol;
let socketURL;
let socket;
let pid;
let decorationHandle;

type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures';

Expand Down Expand Up @@ -147,6 +148,7 @@ if (document.location.pathname === '/test') {
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
document.getElementById('decorations').addEventListener('click', decorationsButtonHandler);
}

function createTerminal(): void {
Expand Down Expand Up @@ -431,3 +433,17 @@ function serializeButtonHandler(): void {
term.write(output);
}
}

function decorationsButtonHandler(): void {
if (decorationHandle) {
term.removeDeoration(decorationHandle);
decorationHandle = null;
} else {
decorationHandle = term.addDecoration({
startColumn: 3, endColumn: 13,
startRow: 3, endRow: 8,
fillStyle: 'rgba(255, 0, 0, 0.7)',
strokeStyle: 'rgba(0, 0, 255, 0.7)',
});
}
}
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ <h3>Options</h3>
<div id="options-container"></div>
</div>
<div>
<button id="decorations">Toggle decorations</button>
<h3>Addons</h3>
<p>Addons can be loaded and unloaded on a particular terminal to extend its functionality.</p>
<div id="addons-container"></div>
Expand Down
26 changes: 24 additions & 2 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IAnsiColorChangeEvent } from 'common/Types';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IAnsiColorChangeEvent, IDecorationElement, IDecorationHandle } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
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 } from 'browser/services/Services';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService, IDecorationService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { IBuffer } from 'common/buffer/Types';
import { MouseService } from 'browser/services/MouseService';
Expand All @@ -55,6 +55,7 @@ import { CoreTerminal } from 'common/CoreTerminal';
import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services';
import { rgba } from 'browser/Color';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { DecorationService } from 'browser/services/DecorationService';

// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
Expand Down Expand Up @@ -86,6 +87,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _characterJoinerService: ICharacterJoinerService | undefined;
private _selectionService: ISelectionService | undefined;
private _soundService: ISoundService | undefined;
private _decorationService: IDecorationService | undefined;

/**
* Records whether the keydown event has already been handled and triggered a data event, if so
Expand Down Expand Up @@ -456,6 +458,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);

this._decorationService = this.register(this._instantiationService.createInstance(DecorationService));
this._instantiationService.setService(IDecorationService, this._decorationService);

const renderer = this._createRenderer();
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
Expand Down Expand Up @@ -515,6 +520,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.viewport!.syncScrollArea();
}
this._selectionService!.refresh();
this._renderService!.onDecorationsChanged();
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));

Expand Down Expand Up @@ -1280,6 +1286,22 @@ export class Terminal extends CoreTerminal implements ITerminal {
// return this.options.bellStyle === 'sound' ||
// this.options.bellStyle === 'both';
}

public addDecoration(element: IDecorationElement): IDecorationHandle | undefined{
const ret = this._decorationService?.addDecoration(element);
this._renderService?.onDecorationsChanged();
return ret;
}
public removeDeoration(handle: IDecorationHandle): boolean {
const ret = !!(this._decorationService?.removeDeoration(handle));
this._renderService?.onDecorationsChanged();
return ret;
}
public clearDecorations(): number {
const ret = this._decorationService?.clearDecorations() || 0;
this._renderService?.onDecorationsChanged();
return ret;
}
}

/**
Expand Down
15 changes: 14 additions & 1 deletion src/browser/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderServic
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, IDecorationElement } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { Terminal } from 'browser/Terminal';
Expand Down Expand Up @@ -198,6 +198,15 @@ export class MockTerminal implements ITerminal {
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
public deregisterCharacterJoiner(joinerId: number): void { }
public addDecoration(element: IDecorationElement): number | undefined {
throw new Error('Method not implemented.');
}
public removeDeoration(handle: number): boolean {
throw new Error('Method not implemented.');
}
public clearDecorations(): number {
throw new Error('Method not implemented.');
}
}

export class MockBuffer implements IBuffer {
Expand Down Expand Up @@ -280,6 +289,7 @@ export class MockRenderer implements IRenderer {
public onBlur(): void { }
public onFocus(): void { }
public onSelectionChanged(start: [number, number], end: [number, number]): void { }
public onDecorationsChanged(): void { }
public onCursorMove(): void { }
public onOptionsChanged(): void { }
public onDevicePixelRatioChange(): void { }
Expand Down Expand Up @@ -402,6 +412,9 @@ export class MockRenderService implements IRenderService {
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
throw new Error('Method not implemented.');
}
public onDecorationsChanged(): void {
throw new Error('Method not implemented.');
}
public onCursorMove(): void {
throw new Error('Method not implemented.');
}
Expand Down
5 changes: 4 additions & 1 deletion src/browser/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { IEvent } from 'common/EventEmitter';
import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types';
import { ICoreTerminal, CharData, ITerminalOptions, IDecorationElement, IDecorationHandle } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
Expand Down Expand Up @@ -81,6 +81,9 @@ export interface IPublicTerminal extends IDisposable {
paste(data: string): void;
refresh(start: number, end: number): void;
reset(): void;
addDecoration(element: IDecorationElement): IDecorationHandle | undefined;
removeDeoration(handle: IDecorationHandle): boolean;
clearDecorations(): number;
}

export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
Expand Down
12 changes: 11 additions & 1 deletion src/browser/public/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, ILinkProvider, IUnicodeHandling, IUnicodeVersionProvider, FontWeight } from 'xterm';
import { ITerminal } from 'browser/Types';
import { IBufferLine, ICellData } from 'common/Types';
import { IBufferLine, ICellData, IDecorationElement, IDecorationHandle } from 'common/Types';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { CellData } from 'common/buffer/CellData';
import { Terminal as TerminalCore } from '../Terminal';
Expand Down Expand Up @@ -175,6 +175,16 @@ export class Terminal implements ITerminalApi {
public paste(data: string): void {
this._core.paste(data);
}
public addDecoration(element: IDecorationElement): IDecorationHandle | undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual API is declared in xterm.d.ts so the new interface will need to go there.

return this._core.addDecoration(element);
}
public removeDeoration(handle: IDecorationHandle): boolean {
return this._core.removeDeoration(handle);
}
Comment on lines +178 to +183
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've started moving towards returning IDisposables for things like this which makes it very convenient to remove the decoration. So something more like this is what I think we should go with:

registerDecoration(range: IBufferRange): IDecoration;
interface IDecoration extends IDisposable {
  // This uses a setter and is implemented by the core so changes can
  // be applied immediately
  range: IBufferRange;
  // This will give ultimate flexibility, perf will probably be fine unless the terminal is covered in decorations
  readonly element: HTMLElement;
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

public clearDecorations(): number {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need a return type here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort of success value for removal - I can remove it.

return this._core.clearDecorations();
}

public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string;
public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean;
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
Expand Down
1 change: 1 addition & 0 deletions src/browser/renderer/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public onCursorMove(): void {}
public onGridChanged(startRow: number, endRow: number): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
public onDecorationsChanged(): void {}

public setColors(colorSet: IColorSet): void {
this._refreshCharAtlas(colorSet);
Expand Down
60 changes: 60 additions & 0 deletions src/browser/renderer/DecorationRenderLayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { IColorSet } from 'browser/Types';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IDecorationService } from 'browser/services/Services';


export class DecorationRenderLayer extends BaseRenderLayer {

private _decorationService: IDecorationService;

constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
@IDecorationService decorationService: IDecorationService,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
) {
super(container, 'decorations', zIndex, true, colors, rendererId, bufferService, optionsService);
this._decorationService = decorationService;
}

public reset(): void {
this._clearAll();
}

public onDecorationsChanged(): void {
// Remove all decorations
this._clearAll();

this._decorationService.forEachDecoration((element) => {
// Translate from buffer position to viewport position
const viewportStartRow = element.startRow - this._bufferService.buffer.ydisp;
const viewportEndRow = element.endRow - this._bufferService.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
const width = element.endColumn - element.startColumn;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
if (viewportStartRow <= viewportEndRow) {
if (element.fillStyle) {
this._ctx.fillStyle = element.fillStyle;
this._fillCells(element.startColumn, viewportCappedStartRow, width, height);
}
if (element.strokeStyle) {
this._ctx.strokeStyle = element.strokeStyle;
this._strokeRectAtCell(element.startColumn, viewportCappedStartRow, width, height);
}
}
return false;
});
}

}
10 changes: 8 additions & 2 deletions src/browser/renderer/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services
import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { DecorationRenderLayer } from 'browser/renderer/DecorationRenderLayer';

let nextRendererId = 1;

Expand All @@ -36,15 +37,16 @@ export class Renderer extends Disposable implements IRenderer {
@IInstantiationService instantiationService: IInstantiationService,
@IBufferService private readonly _bufferService: IBufferService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService
@IOptionsService private readonly _optionsService: IOptionsService,
) {
super();
const allowTransparency = this._optionsService.options.allowTransparency;
this._renderLayers = [
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2),
instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw)
instantiationService.createInstance(DecorationRenderLayer, this._screenElement, 3, this._colors, this._id),
instantiationService.createInstance(CursorRenderLayer, this._screenElement, 4, this._colors, this._id, this._onRequestRedraw),
];
this.dimensions = {
scaledCharWidth: 0,
Expand Down Expand Up @@ -121,6 +123,10 @@ export class Renderer extends Disposable implements IRenderer {
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
}

public onDecorationsChanged(): void {
this._runOperation(l => l.onDecorationsChanged());
}

public onCursorMove(): void {
this._runOperation(l => l.onCursorMove());
}
Expand Down
6 changes: 6 additions & 0 deletions src/browser/renderer/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface IRenderer extends IDisposable {
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onDecorationsChanged(): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
Expand Down Expand Up @@ -91,6 +92,11 @@ export interface IRenderLayer extends IDisposable {
*/
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;

/**
* Called when decorations change
*/
onDecorationsChanged(): void;

/**
* Resize the render layer.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/browser/renderer/dom/DomRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ export class DomRenderer extends Disposable implements IRenderer {
return element;
}

public onDecorationsChanged(): void {
// TBD
}

public onCursorMove(): void {
// No-op, the cursor is drawn when rows are drawn
}
Expand Down
Loading