From ebdd07b8ae0e34027409c7e4bbb6d6b83e4e4100 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Sat, 29 Jan 2022 00:07:51 -0600 Subject: [PATCH] get button to work --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 14 +- src/browser/Terminal.ts | 5 +- src/browser/TestUtils.test.ts | 11 +- src/browser/Types.d.ts | 3 +- src/browser/public/Terminal.ts | 6 +- src/browser/renderer/DecorationRenderLayer.ts | 128 ++++++++++++++++++ src/browser/renderer/Renderer.ts | 13 +- src/browser/renderer/Types.d.ts | 2 + src/browser/renderer/dom/DomRenderer.ts | 10 ++ src/browser/services/RenderService.ts | 5 + src/browser/services/Services.ts | 3 +- typings/xterm.d.ts | 9 +- 12 files changed, 199 insertions(+), 10 deletions(-) create mode 100644 src/browser/renderer/DecorationRenderLayer.ts diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index af1591a8..b1aa03ec 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -13,7 +13,7 @@ import { IWebGL2RenderingContext } from './Types'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; -import { Terminal, IEvent } from 'xterm'; +import { Terminal, IEvent, IBufferDecorationOptions, IDecoration, IGutterDecorationOptions } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; import { ITerminal, IColorSet } from 'browser/Types'; @@ -23,6 +23,8 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { ICharacterJoinerService } from 'browser/services/Services'; import { CharData, ICellData } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; +import { DecorationRenderLayer } from 'browser/renderer/DecorationRenderLayer'; +import { IBufferService } from 'common/services/Services'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -57,10 +59,10 @@ export class WebglRenderer extends Disposable implements IRenderer { super(); this._core = (this._terminal as any)._core; - this._renderLayers = [ new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core), new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw) + // new DecorationRenderLayer(this._core.screenElement!, 3, this._colors, this._id, this._onRequestRedraw) ]; this.dimensions = { scaledCharWidth: 0, @@ -116,6 +118,14 @@ export class WebglRenderer extends Disposable implements IRenderer { return this._charAtlas?.cacheCanvas; } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration { + const decorationLayer = this._renderLayers.find(l => l instanceof DecorationRenderLayer); + if (decorationLayer instanceof DecorationRenderLayer) { + return decorationLayer.registerDecoration(decorationOptions); + } + throw new Error('no decoration layer'); + } + public setColors(colors: IColorSet): void { this._colors = colors; // Clear layers and force a full render diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index fe5c7f79..fb26a2eb 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -37,7 +37,7 @@ import * as Strings from 'browser/LocalizableStrings'; import { SoundService } from 'browser/services/SoundService'; import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; -import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm'; +import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider, IBufferDecorationOptions, IDecoration, IGutterDecorationOptions } from 'xterm'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; @@ -998,6 +998,9 @@ export class Terminal extends CoreTerminal implements ITerminal { return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration | undefined { + return this._renderService?.registerDecoration(decorationOptions); + } /** * Gets whether the terminal has an active selection. */ diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 8fdf458e..c42e4a55 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm'; +import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IBufferDecorationOptions, IDecoration, IGutterDecorationOptions, IDecorationOptions } from 'xterm'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; @@ -102,6 +102,9 @@ export class MockTerminal implements ITerminal { public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { throw new Error('Method not implemented.'); } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration | undefined { + throw new Error('Method not implemented.'); + } public hasSelection(): boolean { throw new Error('Method not implemented.'); } @@ -290,6 +293,9 @@ export class MockRenderer implements IRenderer { public onDevicePixelRatioChange(): void { } public clear(): void { } public renderRows(start: number, end: number): void { } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration { + throw new Error('Method not implemented.'); + } } export class MockViewport implements IViewport { @@ -419,6 +425,9 @@ export class MockRenderService implements IRenderService { public dispose(): void { throw new Error('Method not implemented.'); } + public registerDecoration(decorationOptions: IDecorationOptions): IDecoration { + throw new Error('Method not implemented.'); + } } export class MockCharacterJoinerService implements ICharacterJoinerService { diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index a6165840..779a9454 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDisposable, IMarker, ISelectionPosition } from 'xterm'; +import { IBufferDecorationOptions, IDecoration, IDisposable, IGutterDecorationOptions, IMarker, ISelectionPosition } from 'xterm'; import { IEvent } from 'common/EventEmitter'; import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; @@ -61,6 +61,7 @@ export interface IPublicTerminal extends IDisposable { registerCharacterJoiner(handler: (text: string) => [number, number][]): number; deregisterCharacterJoiner(joinerId: number): void; addMarker(cursorYOffset: number): IMarker | undefined; + registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration | undefined; hasSelection(): boolean; getSelection(): string; getSelectionPosition(): ISelectionPosition | undefined; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 117805f9..1f3c32fe 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm'; +import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IBufferDecorationOptions, IDecoration, IGutterDecorationOptions } from 'xterm'; import { ITerminal } from 'browser/Types'; import { Terminal as TerminalCore } from 'browser/Terminal'; import * as Strings from 'browser/LocalizableStrings'; @@ -171,6 +171,10 @@ export class Terminal implements ITerminalApi { this._verifyIntegers(cursorYOffset); return this._core.addMarker(cursorYOffset); } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration | undefined { + this._checkProposedApi(); + return this._core.registerDecoration(decorationOptions); + } public addMarker(cursorYOffset: number): IMarker | undefined { return this.registerMarker(cursorYOffset); } diff --git a/src/browser/renderer/DecorationRenderLayer.ts b/src/browser/renderer/DecorationRenderLayer.ts new file mode 100644 index 00000000..fd5d32da --- /dev/null +++ b/src/browser/renderer/DecorationRenderLayer.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; +import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IColorSet } from 'browser/Types'; +import { CellData } from 'common/buffer/CellData'; +import { Marker } from 'common/buffer/Marker'; +import { EventEmitter, IEventEmitter } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferService, IOptionsService } from 'common/services/Services'; +import { ICellData } from 'common/Types'; +import { IBufferDecorationOptions, IDecoration, IEvent, IGutterDecorationOptions } from 'xterm'; + +const enum DefaultButton { + WIDTH = 2, + HEIGHT = 1, + COLS = 87, + MARGIN_RIGHT = 3, + COLOR = '#4B9CD3' +} +export class DecorationRenderLayer extends BaseRenderLayer { + private _cell: ICellData = new CellData(); + constructor( + container: HTMLElement, + zIndex: number, + colors: IColorSet, + rendererId: number, + private _onRequestRedraw: IEventEmitter, + @IBufferService bufferService: IBufferService, + @IOptionsService optionsService: IOptionsService + ) { + super(container, 'decoration', zIndex, true, colors, rendererId, bufferService, optionsService); + this.onFocus(); + } + public reset(): void { + + } + + public override onFocus(): void { + this.registerDecoration({ type: 'IBufferDecorationOptions', startMarker: new Marker(1), shape: 'button' }); + this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); + } + + public override resize(dim: IRenderDimensions): void { + super.resize(dim); + this._clearCells(this._bufferService.cols - DefaultButton.MARGIN_RIGHT, 1, 2, 1); + this.registerDecoration({ type: 'IBufferDecorationOptions', startMarker: new Marker(1), shape: 'button' }); + this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); + } + + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration { + if (decorationOptions.type === 'IBufferDecorationOptions') { + const bufferDecoration = new BufferDecoration(decorationOptions.startMarker.line, this._ctx.canvas); + if ('shape' in decorationOptions && decorationOptions.shape === 'button') { + this._ctx.save(); + const color = decorationOptions.color || DefaultButton.COLOR; + const x = 'position' in decorationOptions && decorationOptions.position ? decorationOptions.position : this._bufferService.cols - ((this._bufferService.cols/DefaultButton.COLS) * DefaultButton.MARGIN_RIGHT); + if (x && color) { + this._ctx.fillStyle = color; + this._fillCells(x, decorationOptions.startMarker.line, DefaultButton.WIDTH, DefaultButton.HEIGHT); + this._ctx.fillStyle = color; + this._fillCharTrueColor(this._cell, x, decorationOptions.startMarker.line); + addDisposableDomListener(this._ctx.canvas, 'click', e => { + e.stopPropagation(); + const { x, y } = getRelativeClickPosition(this._ctx.canvas, e); + if (this._ctx.isPointInPath(x, y)) { + console.log('clicked button'); + } + }); + } + this._ctx.restore(); + return bufferDecoration; + } + throw new Error('Border box type not yet implemented'); + } throw new Error('Gutter decoration not yet implemented'); + } +} + +function getRelativeClickPosition(canvas: HTMLCanvasElement, event: MouseEvent): { x: number, y: number } { + const rect = canvas.getBoundingClientRect(); + const y = event.clientY - rect.top; + const x = event.clientX - rect.left; + return { x, y }; +} + +export class BufferDecoration extends Disposable implements IDecoration { + private static _nextId = 1; + + private _element: HTMLElement | undefined; + private _id: number = BufferDecoration._nextId++; + public isDisposed: boolean = false; + + public get id(): number { return this._id; } + + public get element(): HTMLElement { return this.element!; } + + 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( + public line: number, + container: HTMLElement + ) { + super(); + this._element = document.createElement('menu'); + this._element.textContent = 'hello'; + this._element.id = 'decoration' + this._id; + container.appendChild(this._element); + } + + public dispose(): void { + if (this.isDisposed) { + return; + } + this.isDisposed = true; + this.line = -1; + // Emit before super.dispose such that dispose listeners get a change to react + this._onDispose.fire(); + super.dispose(); + } +} diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index 7a64257d..4829e0bb 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -14,6 +14,8 @@ 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'; +import { IBufferDecorationOptions, IGutterDecorationOptions, IDecoration } from 'xterm'; let nextRendererId = 1; @@ -44,7 +46,8 @@ export class Renderer extends Disposable implements IRenderer { 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(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw), + instantiationService.createInstance(DecorationRenderLayer, this._screenElement, 4, this._colors, this._id, this._onRequestRedraw) ]; this.dimensions = { scaledCharWidth: 0, @@ -65,6 +68,14 @@ export class Renderer extends Disposable implements IRenderer { this.onOptionsChanged(); } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration { + const decorationLayer = this._renderLayers.find(l => l instanceof DecorationRenderLayer); + if (decorationLayer instanceof DecorationRenderLayer) { + return decorationLayer.registerDecoration(decorationOptions); + } + throw new Error('no decoration layer'); + } + public dispose(): void { for (const l of this._renderLayers) { l.dispose(); diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index 6818a926..8a328686 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -6,6 +6,7 @@ import { IDisposable } from 'common/Types'; import { IColorSet } from 'browser/Types'; import { IEvent } from 'common/EventEmitter'; +import { IBufferDecorationOptions, IDecoration, IGutterDecorationOptions } from 'xterm'; export interface IRenderDimensions { scaledCharWidth: number; @@ -53,6 +54,7 @@ export interface IRenderer extends IDisposable { clear(): void; renderRows(start: number, end: number): void; clearTextureAtlas?(): void; + registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration; } export interface IRenderLayer extends IDisposable { diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index ee283399..58c54f65 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -13,6 +13,8 @@ import { IOptionsService, IBufferService, IInstantiationService } from 'common/s import { EventEmitter, IEvent } from 'common/EventEmitter'; import { color } from 'browser/Color'; import { removeElementFromParent } from 'browser/Dom'; +import { DecorationRenderLayer } from 'browser/renderer/DecorationRenderLayer'; +import { IBufferDecorationOptions, IGutterDecorationOptions, IDecoration } from 'xterm'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -151,6 +153,14 @@ export class DomRenderer extends Disposable implements IRenderer { this._injectCss(); } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration { + // const decorationLayer = this._renderLayers.find(l => l instanceof DecorationRenderLayer); + // if (decorationLayer instanceof DecorationRenderLayer) { + // return decorationLayer.registerDecoration(decorationOptions); + // } + throw new Error('no decoration layer'); + } + private _injectCss(): void { if (!this._themeStyleElement) { this._themeStyleElement = document.createElement('style'); diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index b8283e0e..526bffa3 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -12,6 +12,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncer } from 'browser/Types'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; +import { IDecorationOptions, IDecoration, IGutterDecorationOptions, IBufferDecorationOptions } from 'xterm'; interface ISelectionState { start: [number, number] | undefined; @@ -85,6 +86,10 @@ export class RenderService extends Disposable implements IRenderService { } } + public registerDecoration(decorationOptions: IBufferDecorationOptions | IGutterDecorationOptions): IDecoration { + return this._renderer.registerDecoration(decorationOptions); + } + private _onIntersectionChange(entry: IntersectionObserverEntry): void { this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting; diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 4928fa28..c3ec2eb5 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -9,6 +9,7 @@ import { IColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDisposable } from 'common/Types'; +import { IDecoration, IDecorationOptions } from 'xterm'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { @@ -51,7 +52,7 @@ export interface IRenderService extends IDisposable { onRefreshRequest: IEvent<{ start: number, end: number }>; dimensions: IRenderDimensions; - + registerDecoration(decorationOptions: IDecorationOptions): IDecoration; refreshRows(start: number, end: number): void; clearTextureAtlas(): void; resize(cols: number, rows: number): void; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 83c1d10c..6b839299 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -428,7 +428,12 @@ declare module 'xterm' { onRender: IEvent; } - export interface IDecorationOptions extends IDisposable { + export interface IDecorationOptions { + /** + * The type of decoration options + */ + type: 'IBufferDecorationOptions' | 'IGutterDecorationOptions'; + /** * The line in the terminal where * the decoration will be displayed @@ -451,7 +456,7 @@ declare module 'xterm' { /** * The type of buffer decoration */ - type: 'button' | 'box-border'; + shape: 'button' | 'box-border'; /* * The x position for the decoration.