diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 7efcd74e..19b07b5f 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -14,6 +14,7 @@ import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { Terminal } from 'xterm'; import { toDisposable } from 'common/Lifecycle'; import { isFirefox } from 'common/Platform'; +import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager'; interface ICursorState { x: number; @@ -60,6 +61,7 @@ export class CursorRenderLayer extends BaseRenderLayer { 'underline': this._renderUnderlineCursor.bind(this) }; this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); + this._handleOptionsChanged(); this.register(toDisposable(() => { this._cursorBlinkStateManager?.dispose(); this._cursorBlinkStateManager = undefined; @@ -97,9 +99,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _handleOptionsChanged(): void { if (this._optionsService.rawOptions.cursorBlink) { if (!this._cursorBlinkStateManager) { - this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => { - this._render(true); - }, this._coreBrowserService); + this._cursorBlinkStateManager = new CursorBlinkStateManager(() => this._render(true), this._coreBrowserService); } } else { this._cursorBlinkStateManager?.dispose(); @@ -238,139 +238,3 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.restore(); } } - -class CursorBlinkStateManager { - public isCursorVisible: boolean; - - private _animationFrame: number | undefined; - private _blinkStartTimeout: number | undefined; - private _blinkInterval: number | undefined; - - /** - * The time at which the animation frame was restarted, this is used on the - * next render to restart the timers so they don't need to restart the timers - * multiple times over a short period. - */ - private _animationTimeRestarted: number | undefined; - - constructor( - isFocused: boolean, - private _renderCallback: () => void, - private _coreBrowserService: ICoreBrowserService - ) { - this.isCursorVisible = true; - if (isFocused) { - this._restartInterval(); - } - } - - public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); } - - public dispose(): void { - if (this._blinkInterval) { - this._coreBrowserService.window.clearInterval(this._blinkInterval); - this._blinkInterval = undefined; - } - if (this._blinkStartTimeout) { - this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = undefined; - } - if (this._animationFrame) { - this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = undefined; - } - } - - public restartBlinkAnimation(): void { - if (this.isPaused) { - return; - } - // Save a timestamp so that the restart can be done on the next interval - this._animationTimeRestarted = Date.now(); - // Force a cursor render to ensure it's visible and in the correct position - this.isCursorVisible = true; - if (!this._animationFrame) { - this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { - this._renderCallback(); - this._animationFrame = undefined; - }); - } - } - - private _restartInterval(timeToStart: number = BLINK_INTERVAL): void { - // Clear any existing interval - if (this._blinkInterval) { - this._coreBrowserService.window.clearInterval(this._blinkInterval); - this._blinkInterval = undefined; - } - - // Setup the initial timeout which will hide the cursor, this is done before - // the regular interval is setup in order to support restarting the blink - // animation in a lightweight way (without thrashing clearInterval and - // setInterval). - this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => { - // Check if another animation restart was requested while this was being - // started - if (this._animationTimeRestarted) { - const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = undefined; - if (time > 0) { - this._restartInterval(time); - return; - } - } - - // Hide the cursor - this.isCursorVisible = false; - this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { - this._renderCallback(); - this._animationFrame = undefined; - }); - - // Setup the blink interval - this._blinkInterval = this._coreBrowserService.window.setInterval(() => { - // Adjust the animation time if it was restarted - if (this._animationTimeRestarted) { - // calc time diff - // Make restart interval do a setTimeout initially? - const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = undefined; - this._restartInterval(time); - return; - } - - // Invert visibility and render - this.isCursorVisible = !this.isCursorVisible; - this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { - this._renderCallback(); - this._animationFrame = undefined; - }); - }, BLINK_INTERVAL); - }, timeToStart); - } - - public pause(): void { - this.isCursorVisible = true; - if (this._blinkInterval) { - this._coreBrowserService.window.clearInterval(this._blinkInterval); - this._blinkInterval = undefined; - } - if (this._blinkStartTimeout) { - this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = undefined; - } - if (this._animationFrame) { - this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = undefined; - } - } - - public resume(): void { - // Clear out any existing timers just in case - this.pause(); - - this._animationTimeRestarted = undefined; - this._restartInterval(); - this.restartBlinkAnimation(); - } -} diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 202d4deb..80a631e0 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -308,6 +308,7 @@ export class GlyphRenderer extends Disposable { public handleResize(): void { const gl = this._gl; + gl.useProgram(this._program); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); this.clear(); diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index fb5a0762..4706b442 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -50,16 +50,21 @@ void main() { outColor = v_color; }`; -interface IVertices { - attributes: Float32Array; - count: number; -} - const INDICES_PER_RECTANGLE = 8; const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEMENT; const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE; +class Vertices { + public attributes: Float32Array; + public count: number; + + constructor() { + this.attributes = new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY); + this.count = 0; + } +} + // Work variables to avoid garbage collection let $rgba = 0; let $isDefault = false; @@ -77,11 +82,10 @@ export class RectangleRenderer extends Disposable { private _attributesBuffer: WebGLBuffer; private _projectionLocation: WebGLUniformLocation; private _bgFloat!: Float32Array; + private _cursorFloat!: Float32Array; - private _vertices: IVertices = { - count: 0, - attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY) - }; + private _vertices: Vertices = new Vertices(); + private _verticesCursor: Vertices = new Vertices(); constructor( private _terminal: Terminal, @@ -142,7 +146,15 @@ export class RectangleRenderer extends Disposable { })); } - public render(): void { + public renderBackgrounds(): void { + this._renderVertices(this._vertices); + } + + public renderCursor(): void { + this._renderVertices(this._verticesCursor); + } + + private _renderVertices(vertices: Vertices): void { const gl = this._gl; gl.useProgram(this._program); @@ -153,8 +165,8 @@ export class RectangleRenderer extends Disposable { // Bind attributes buffer and draw gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); - gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW); - gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count); + gl.bufferData(gl.ARRAY_BUFFER, vertices.attributes, gl.DYNAMIC_DRAW); + gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, vertices.count); } public handleResize(): void { @@ -167,6 +179,7 @@ export class RectangleRenderer extends Disposable { private _updateCachedColors(colors: ReadonlyColorSet): void { this._bgFloat = this._colorToFloat32Array(colors.background); + this._cursorFloat = this._colorToFloat32Array(colors.cursor); } private _updateViewportRectangle(): void { @@ -231,7 +244,72 @@ export class RectangleRenderer extends Disposable { vertices.count = rectangleCount; } - private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { + public updateCursor(model: IRenderModel): void { + const vertices = this._verticesCursor; + const cursor = model.cursor; + if (!cursor || cursor.style === 'block') { + vertices.count = 0; + return; + } + + let offset: number; + let rectangleCount = 0; + + if (cursor.style === 'bar' || cursor.style === 'blur') { + // Left edge + offset = rectangleCount++ * INDICES_PER_RECTANGLE; + this._addRectangleFloat( + vertices.attributes, + offset, + cursor.x * this._dimensions.device.cell.width, + cursor.y * this._dimensions.device.cell.height, + cursor.style === 'bar' ? cursor.dpr * cursor.cursorWidth : cursor.dpr, + this._dimensions.device.cell.height, + this._cursorFloat + ); + } + if (cursor.style === 'underline' || cursor.style === 'blur') { + // Bottom edge + offset = rectangleCount++ * INDICES_PER_RECTANGLE; + this._addRectangleFloat( + vertices.attributes, + offset, + cursor.x * this._dimensions.device.cell.width, + (cursor.y + 1) * this._dimensions.device.cell.height - cursor.dpr, + cursor.width * this._dimensions.device.cell.width, + cursor.dpr, + this._cursorFloat + ); + } + if (cursor.style === 'blur') { + // Top edge + offset = rectangleCount++ * INDICES_PER_RECTANGLE; + this._addRectangleFloat( + vertices.attributes, + offset, + cursor.x * this._dimensions.device.cell.width, + cursor.y * this._dimensions.device.cell.height, + cursor.width * this._dimensions.device.cell.width, + cursor.dpr, + this._cursorFloat + ); + // Right edge + offset = rectangleCount++ * INDICES_PER_RECTANGLE; + this._addRectangleFloat( + vertices.attributes, + offset, + (cursor.x + cursor.width) * this._dimensions.device.cell.width - cursor.dpr, + cursor.y * this._dimensions.device.cell.height, + cursor.dpr, + this._dimensions.device.cell.height, + this._cursorFloat + ); + } + + vertices.count = rectangleCount; + } + + private _updateRectangle(vertices: Vertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { $isDefault = false; if (fg & FgFlags.INVERSE) { switch (fg & Attributes.CM_MASK) { diff --git a/addons/xterm-addon-webgl/src/RenderModel.ts b/addons/xterm-addon-webgl/src/RenderModel.ts index db364db4..a94f317e 100644 --- a/addons/xterm-addon-webgl/src/RenderModel.ts +++ b/addons/xterm-addon-webgl/src/RenderModel.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderModel } from './Types'; +import { ICursorRenderModel, IRenderModel } from './Types'; import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; @@ -18,6 +18,7 @@ export class RenderModel implements IRenderModel { public cells: Uint32Array; public lineLengths: Uint32Array; public selection: ISelectionRenderModel; + public cursor?: ICursorRenderModel; constructor() { this.cells = new Uint32Array(0); diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts index 6e3a18fc..73b15bc7 100644 --- a/addons/xterm-addon-webgl/src/Types.d.ts +++ b/addons/xterm-addon-webgl/src/Types.d.ts @@ -9,6 +9,16 @@ export interface IRenderModel { cells: Uint32Array; lineLengths: Uint32Array; selection: ISelectionRenderModel; + cursor?: ICursorRenderModel; +} + +export interface ICursorRenderModel { + x: number; + y: number; + width: number; + style: string; + cursorWidth: number; + dpr: number; } export interface IWebGL2RenderingContext extends WebGLRenderingContext { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 4fc4427f..b74aada9 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -14,7 +14,7 @@ import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeS import { ITerminal } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; -import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -22,7 +22,7 @@ import { CharData, IBufferLine, ICellData } from 'common/Types'; import { IDisposable, Terminal } from 'xterm'; import { GlyphRenderer } from './GlyphRenderer'; import { RectangleRenderer } from './RectangleRenderer'; -import { CursorRenderLayer } from './renderLayer/CursorRenderLayer'; +import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager'; import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { IRenderLayer } from './renderLayer/Types'; import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; @@ -30,6 +30,7 @@ import { IWebGL2RenderingContext } from './Types'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; + private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; private _charAtlasDisposable: IDisposable | undefined; private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; @@ -65,7 +66,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private readonly _characterJoinerService: ICharacterJoinerService, private readonly _charSizeService: ICharSizeService, private readonly _coreBrowserService: ICoreBrowserService, - coreService: ICoreService, + private readonly _coreService: ICoreService, private readonly _decorationService: IDecorationService, private readonly _optionsService: IOptionsService, private readonly _themeService: IThemeService, @@ -80,12 +81,12 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core = (this._terminal as any)._core; this._renderLayers = [ - new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService), - new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, _optionsService, this._themeService) + new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService) ]; this.dimensions = createRenderDimensions(); this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); + this._updateCursorBlink(); this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged())); this._canvas = document.createElement('canvas'); @@ -201,6 +202,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.handleBlur(this._terminal); } + this._cursorBlinkStateManager?.pause(); // Request a redraw for active/inactive selection background this._requestRedrawViewport(); } @@ -209,6 +211,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.handleFocus(this._terminal); } + this._cursorBlinkStateManager?.resume(); // Request a redraw for active/inactive selection background this._requestRedrawViewport(); } @@ -225,11 +228,13 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.handleCursorMove(this._terminal); } + this._cursorBlinkStateManager?.restartBlinkAnimation(); } private _handleOptionsChanged(): void { this._updateDimensions(); this._refreshCharAtlas(); + this._updateCursorBlink(); } /** @@ -305,6 +310,9 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.reset(this._terminal); } + + this._cursorBlinkStateManager?.restartBlinkAnimation(); + this._updateCursorBlink(); } public registerCharacterJoiner(handler: (text: string) => [number, number][]): number { @@ -347,8 +355,27 @@ export class WebglRenderer extends Disposable implements IRenderer { } // Render - this._rectangleRenderer?.render(); + this._rectangleRenderer?.renderBackgrounds(); this._glyphRenderer?.render(this._model); + if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isCursorVisible) { + this._rectangleRenderer?.renderCursor(); + } + } + + private _updateCursorBlink(): void { + if (this._terminal.options.cursorBlink) { + if (!this._cursorBlinkStateManager) { + this._cursorBlinkStateManager = new CursorBlinkStateManager(() => { + this._requestRedrawCursor(); + }, this._coreBrowserService); + } + } else { + this._cursorBlinkStateManager?.dispose(); + this._cursorBlinkStateManager = undefined; + } + // Request a refresh from the terminal as management of rendering is being + // moved back to the terminal + this._requestRedrawCursor(); } private _updateModel(start: number, end: number): void { @@ -371,6 +398,18 @@ export class WebglRenderer extends Disposable implements IRenderer { let j: number; start = clamp(start, terminal.rows - 1, 0); end = clamp(end, terminal.rows - 1, 0); + + const cursorY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY; + // in case cursor.x == cols adjust visual cursor to cols - 1 + const cursorX = Math.min(this._terminal.buffer.active.cursorX, terminal.cols - 1); + let lastCursorX = -1; + const isCursorVisible = + this._coreService.isCursorInitialized && + !this._coreService.isCursorHidden && + (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isCursorVisible); + this._model.cursor = undefined; + let modelUpdated = false; + for (y = start; y <= end; y++) { row = y + terminal.buffer.ydisp; line = terminal.buffer.lines.get(row)!; @@ -414,6 +453,30 @@ export class WebglRenderer extends Disposable implements IRenderer { // Load colors/resolve overrides into work colors this._cellColorResolver.resolve(cell, x, row); + // Override colors for cursor cell + if (isCursorVisible && row === cursorY) { + if (x === cursorX) { + this._model.cursor = { + x: cursorX, + y: this._terminal.buffer.active.cursorY, + width: cell.getWidth(), + style: this._coreBrowserService.isFocused ? + (terminal.options.cursorStyle || 'block') : 'blur', + cursorWidth: terminal.options.cursorWidth, + dpr: this._devicePixelRatio + }; + lastCursorX = cursorX + cell.getWidth() - 1; + } + if (x >= cursorX && x <= lastCursorX && + this._coreBrowserService.isFocused && + (terminal.options.cursorStyle || 'block') === 'block') { + this._cellColorResolver.result.fg = + Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK); + this._cellColorResolver.result.bg = + Attributes.CM_RGB | (this._themeService.colors.cursor.rgba >> 8 & Attributes.RGB_MASK); + } + } + if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; } @@ -426,6 +489,8 @@ export class WebglRenderer extends Disposable implements IRenderer { continue; } + modelUpdated = true; + // Flag combined chars with a bit mask so they're easily identifiable if (chars.length > 1) { code |= COMBINED_CHAR_BIT_MASK; @@ -455,7 +520,10 @@ export class WebglRenderer extends Disposable implements IRenderer { } } } - this._rectangleRenderer!.updateBackgrounds(this._model); + if (modelUpdated) { + this._rectangleRenderer!.updateBackgrounds(this._model); + } + this._rectangleRenderer!.updateCursor(this._model); } /** @@ -527,6 +595,11 @@ export class WebglRenderer extends Disposable implements IRenderer { private _requestRedrawViewport(): void { this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); } + + private _requestRedrawCursor(): void { + const cursorY = this._terminal.buffer.active.cursorY; + this._onRequestRedraw.fire({ start: cursorY, end: cursorY }); + } } // TODO: Share impl with core diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 1a45eb5e..6296e2f1 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -121,21 +121,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer public abstract reset(terminal: Terminal): void; - /** - * Fills 1+ cells completely. This uses the existing fillStyle on the context. - * @param x The column to start at. - * @param y The row to start at - * @param width The number of columns to fill. - * @param height The number of rows to fill. - */ - protected _fillCells(x: number, y: number, width: number, height: number): void { - this._ctx.fillRect( - x * this._deviceCellWidth, - y * this._deviceCellHeight, - width * this._deviceCellWidth, - height * this._deviceCellHeight); - } - /** * Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the * existing fillStyle on the context. @@ -150,35 +135,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._coreBrowserService.dpr); } - /** - * Fills a 1px line (2px on HDPI) at the left of the cell. This uses the - * existing fillStyle on the context. - * @param x The column to fill. - * @param y The row to fill. - */ - protected _fillLeftLineAtCell(x: number, y: number, width: number): void { - this._ctx.fillRect( - x * this._deviceCellWidth, - y * this._deviceCellHeight, - this._coreBrowserService.dpr * width, - this._deviceCellHeight); - } - - /** - * Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing - * strokeStyle on the context. - * @param x The column to fill. - * @param y The row to fill. - */ - protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void { - this._ctx.lineWidth = this._coreBrowserService.dpr; - this._ctx.strokeRect( - x * this._deviceCellWidth + this._coreBrowserService.dpr / 2, - y * this._deviceCellHeight + (this._coreBrowserService.dpr / 2), - width * this._deviceCellWidth - this._coreBrowserService.dpr, - (height * this._deviceCellHeight) - this._coreBrowserService.dpr); - } - /** * Clears the entire canvas. */ diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts deleted file mode 100644 index c6a33a5c..00000000 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ /dev/null @@ -1,375 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; -import { BaseRenderLayer } from './BaseRenderLayer'; -import { ICellData } from 'common/Types'; -import { CellData } from 'common/buffer/CellData'; -import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; -import { IEventEmitter } from 'common/EventEmitter'; -import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { ICoreService, IOptionsService } from 'common/services/Services'; -import { toDisposable } from 'common/Lifecycle'; -import { isFirefox } from 'common/Platform'; - -interface ICursorState { - x: number; - y: number; - isFocused: boolean; - style: string; - width: number; -} - -/** - * The time between cursor blinks. - */ -const BLINK_INTERVAL = 600; - -export class CursorRenderLayer extends BaseRenderLayer { - private _state: ICursorState; - private _cursorRenderers: {[key: string]: (terminal: Terminal, x: number, y: number, cell: ICellData) => void}; - private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; - private _cell: ICellData = new CellData(); - - constructor( - terminal: Terminal, - container: HTMLElement, - zIndex: number, - private _onRequestRefreshRowsEvent: IEventEmitter, - coreBrowserService: ICoreBrowserService, - private readonly _coreService: ICoreService, - optionsService: IOptionsService, - themeService: IThemeService - ) { - super(terminal, container, 'cursor', zIndex, true, coreBrowserService, optionsService, themeService); - this._state = { - x: 0, - y: 0, - isFocused: false, - style: '', - width: 0 - }; - this._cursorRenderers = { - 'bar': this._renderBarCursor.bind(this), - 'block': this._renderBlockCursor.bind(this), - 'underline': this._renderUnderlineCursor.bind(this) - }; - this._handleOptionsChanged(terminal); - this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal))); - this.register(toDisposable(() => { - this._cursorBlinkStateManager?.dispose(); - this._cursorBlinkStateManager = undefined; - })); - } - - public resize(terminal: Terminal, dim: IRenderDimensions): void { - super.resize(terminal, dim); - // Resizing the canvas discards the contents of the canvas so clear state - this._state = { - x: 0, - y: 0, - isFocused: false, - style: '', - width: 0 - }; - } - - public reset(terminal: Terminal): void { - this._clearCursor(); - this._cursorBlinkStateManager?.restartBlinkAnimation(terminal); - this._handleOptionsChanged(terminal); - } - - public handleBlur(terminal: Terminal): void { - this._cursorBlinkStateManager?.pause(); - this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY }); - } - - public handleFocus(terminal: Terminal): void { - this._cursorBlinkStateManager?.resume(terminal); - this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY }); - } - - private _handleOptionsChanged(terminal: Terminal): void { - if (terminal.options.cursorBlink) { - if (!this._cursorBlinkStateManager) { - this._cursorBlinkStateManager = new CursorBlinkStateManager(() => { - this._render(terminal, true); - }, this._coreBrowserService); - } - } else { - this._cursorBlinkStateManager?.dispose(); - this._cursorBlinkStateManager = undefined; - } - // Request a refresh from the terminal as management of rendering is being - // moved back to the terminal - this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY }); - } - - public handleCursorMove(terminal: Terminal): void { - this._cursorBlinkStateManager?.restartBlinkAnimation(terminal); - } - - public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void { - if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) { - this._render(terminal, false); - } else { - this._cursorBlinkStateManager.restartBlinkAnimation(terminal); - } - } - - private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void { - // Don't draw the cursor if it's hidden - if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) { - this._clearCursor(); - return; - } - - const cursorY = terminal.buffer.active.baseY + terminal.buffer.active.cursorY; - const viewportRelativeCursorY = cursorY - terminal.buffer.active.viewportY; - - // in case cursor.x == cols adjust visual cursor to cols - 1 - const cursorX = Math.min(terminal.buffer.active.cursorX, terminal.cols - 1); - - // Don't draw the cursor if it's off-screen - if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) { - this._clearCursor(); - return; - } - - // TODO: Need fast buffer API for loading cell - (terminal as any)._core.buffer.lines.get(cursorY).loadCell(cursorX, this._cell); - if (this._cell.content === undefined) { - return; - } - - if (!this._coreBrowserService.isFocused) { - this._clearCursor(); - this._ctx.save(); - this._ctx.fillStyle = this._themeService.colors.cursor.css; - const cursorStyle = terminal.options.cursorStyle; - this._renderBlurCursor(terminal, cursorX, viewportRelativeCursorY, this._cell); - this._ctx.restore(); - this._state.x = cursorX; - this._state.y = viewportRelativeCursorY; - this._state.isFocused = false; - this._state.style = cursorStyle!; - this._state.width = this._cell.getWidth(); - return; - } - - // Don't draw the cursor if it's blinking - if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) { - this._clearCursor(); - return; - } - - if (this._state) { - // The cursor is already in the correct spot, don't redraw - if (this._state.x === cursorX && - this._state.y === viewportRelativeCursorY && - this._state.isFocused === this._coreBrowserService.isFocused && - this._state.style === terminal.options.cursorStyle && - this._state.width === this._cell.getWidth()) { - return; - } - this._clearCursor(); - } - - this._ctx.save(); - this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell); - this._ctx.restore(); - - this._state.x = cursorX; - this._state.y = viewportRelativeCursorY; - this._state.isFocused = false; - this._state.style = terminal.options.cursorStyle!; - this._state.width = this._cell.getWidth(); - } - - private _clearCursor(): void { - if (this._state) { - // Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is - // less than 1 - if (isFirefox || this._coreBrowserService.dpr < 1) { - this._clearAll(); - } else { - this._clearCells(this._state.x, this._state.y, this._state.width, 1); - } - this._state = { - x: 0, - y: 0, - isFocused: false, - style: '', - width: 0 - }; - } - } - - private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { - this._ctx.save(); - this._ctx.fillStyle = this._themeService.colors.cursor.css; - this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth); - this._ctx.restore(); - } - - private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { - this._ctx.save(); - this._ctx.fillStyle = this._themeService.colors.cursor.css; - this._fillCells(x, y, cell.getWidth(), 1); - this._ctx.fillStyle = this._themeService.colors.cursorAccent.css; - this._fillCharTrueColor(terminal, cell, x, y); - this._ctx.restore(); - } - - private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { - this._ctx.save(); - this._ctx.fillStyle = this._themeService.colors.cursor.css; - this._fillBottomLineAtCells(x, y); - this._ctx.restore(); - } - - private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { - this._ctx.save(); - this._ctx.strokeStyle = this._themeService.colors.cursor.css; - this._strokeRectAtCell(x, y, cell.getWidth(), 1); - this._ctx.restore(); - } -} - -class CursorBlinkStateManager { - public isCursorVisible: boolean; - - private _animationFrame: number | undefined; - private _blinkStartTimeout: number | undefined; - private _blinkInterval: number | undefined; - - /** - * The time at which the animation frame was restarted, this is used on the - * next render to restart the timers so they don't need to restart the timers - * multiple times over a short period. - */ - private _animationTimeRestarted: number | undefined; - - constructor( - private _renderCallback: () => void, - private _coreBrowserService: ICoreBrowserService - ) { - this.isCursorVisible = true; - if (this._coreBrowserService.isFocused) { - this._restartInterval(); - } - } - - public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); } - - public dispose(): void { - if (this._blinkInterval) { - this._coreBrowserService.window.clearInterval(this._blinkInterval); - this._blinkInterval = undefined; - } - if (this._blinkStartTimeout) { - this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = undefined; - } - if (this._animationFrame) { - this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = undefined; - } - } - - public restartBlinkAnimation(terminal: Terminal): void { - if (this.isPaused) { - return; - } - // Save a timestamp so that the restart can be done on the next interval - this._animationTimeRestarted = Date.now(); - // Force a cursor render to ensure it's visible and in the correct position - this.isCursorVisible = true; - if (!this._animationFrame) { - this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { - this._renderCallback(); - this._animationFrame = undefined; - }); - } - } - - private _restartInterval(timeToStart: number = BLINK_INTERVAL): void { - // Clear any existing interval - if (this._blinkInterval) { - this._coreBrowserService.window.clearInterval(this._blinkInterval); - this._blinkInterval = undefined; - } - - // Setup the initial timeout which will hide the cursor, this is done before - // the regular interval is setup in order to support restarting the blink - // animation in a lightweight way (without thrashing clearInterval and - // setInterval). - this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => { - // Check if another animation restart was requested while this was being - // started - if (this._animationTimeRestarted) { - const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = undefined; - if (time > 0) { - this._restartInterval(time); - return; - } - } - - // Hide the cursor - this.isCursorVisible = false; - this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { - this._renderCallback(); - this._animationFrame = undefined; - }); - - // Setup the blink interval - this._blinkInterval = this._coreBrowserService.window.setInterval(() => { - // Adjust the animation time if it was restarted - if (this._animationTimeRestarted) { - // calc time diff - // Make restart interval do a setTimeout initially? - const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = undefined; - this._restartInterval(time); - return; - } - - // Invert visibility and render - this.isCursorVisible = !this.isCursorVisible; - this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { - this._renderCallback(); - this._animationFrame = undefined; - }); - }, BLINK_INTERVAL); - }, timeToStart); - } - - public pause(): void { - this.isCursorVisible = true; - if (this._blinkInterval) { - this._coreBrowserService.window.clearInterval(this._blinkInterval); - this._blinkInterval = undefined; - } - if (this._blinkStartTimeout) { - this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = undefined; - } - if (this._animationFrame) { - this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = undefined; - } - } - - public resume(terminal: Terminal): void { - // Clear out any existing timers just in case - this.pause(); - - this._animationTimeRestarted = undefined; - this._restartInterval(); - this.restartBlinkAnimation(terminal); - } -} diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 3edfdc33..cca37e0a 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -25,7 +25,7 @@ describe('WebGL Renderer Integration Tests', async () => { itWebgl('dispose removes renderer canvases', async function(): Promise { await setupBrowser(); - assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3); + assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); await page.evaluate(`addon.dispose()`); assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); await browser.close(); diff --git a/src/browser/renderer/shared/CursorBlinkStateManager.ts b/src/browser/renderer/shared/CursorBlinkStateManager.ts new file mode 100644 index 00000000..c5bb0870 --- /dev/null +++ b/src/browser/renderer/shared/CursorBlinkStateManager.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreBrowserService } from 'browser/services/Services'; + +/** + * The time between cursor blinks. + */ +const BLINK_INTERVAL = 600; + +export class CursorBlinkStateManager { + public isCursorVisible: boolean; + + private _animationFrame: number | undefined; + private _blinkStartTimeout: number | undefined; + private _blinkInterval: number | undefined; + + /** + * The time at which the animation frame was restarted, this is used on the + * next render to restart the timers so they don't need to restart the timers + * multiple times over a short period. + */ + private _animationTimeRestarted: number | undefined; + + constructor( + private _renderCallback: () => void, + private _coreBrowserService: ICoreBrowserService + ) { + this.isCursorVisible = true; + if (this._coreBrowserService.isFocused) { + this._restartInterval(); + } + } + + public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); } + + public dispose(): void { + if (this._blinkInterval) { + this._coreBrowserService.window.clearInterval(this._blinkInterval); + this._blinkInterval = undefined; + } + if (this._blinkStartTimeout) { + this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); + this._blinkStartTimeout = undefined; + } + if (this._animationFrame) { + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = undefined; + } + } + + public restartBlinkAnimation(): void { + if (this.isPaused) { + return; + } + // Save a timestamp so that the restart can be done on the next interval + this._animationTimeRestarted = Date.now(); + // Force a cursor render to ensure it's visible and in the correct position + this.isCursorVisible = true; + if (!this._animationFrame) { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { + this._renderCallback(); + this._animationFrame = undefined; + }); + } + } + + private _restartInterval(timeToStart: number = BLINK_INTERVAL): void { + // Clear any existing interval + if (this._blinkInterval) { + this._coreBrowserService.window.clearInterval(this._blinkInterval); + this._blinkInterval = undefined; + } + + // Setup the initial timeout which will hide the cursor, this is done before + // the regular interval is setup in order to support restarting the blink + // animation in a lightweight way (without thrashing clearInterval and + // setInterval). + this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => { + // Check if another animation restart was requested while this was being + // started + if (this._animationTimeRestarted) { + const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); + this._animationTimeRestarted = undefined; + if (time > 0) { + this._restartInterval(time); + return; + } + } + + // Hide the cursor + this.isCursorVisible = false; + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { + this._renderCallback(); + this._animationFrame = undefined; + }); + + // Setup the blink interval + this._blinkInterval = this._coreBrowserService.window.setInterval(() => { + // Adjust the animation time if it was restarted + if (this._animationTimeRestarted) { + // calc time diff + // Make restart interval do a setTimeout initially? + const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); + this._animationTimeRestarted = undefined; + this._restartInterval(time); + return; + } + + // Invert visibility and render + this.isCursorVisible = !this.isCursorVisible; + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { + this._renderCallback(); + this._animationFrame = undefined; + }); + }, BLINK_INTERVAL); + }, timeToStart); + } + + public pause(): void { + this.isCursorVisible = true; + if (this._blinkInterval) { + this._coreBrowserService.window.clearInterval(this._blinkInterval); + this._blinkInterval = undefined; + } + if (this._blinkStartTimeout) { + this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); + this._blinkStartTimeout = undefined; + } + if (this._animationFrame) { + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = undefined; + } + } + + public resume(): void { + // Clear out any existing timers just in case + this.pause(); + + this._animationTimeRestarted = undefined; + this._restartInterval(); + this.restartBlinkAnimation(); + } +}