Skip to content

Commit

Permalink
Render the cursor in the WebGL canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
xzfc committed Jun 18, 2023
1 parent ac923e5 commit 064fe0c
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 439 deletions.
147 changes: 147 additions & 0 deletions addons/xterm-addon-webgl/src/CursorBlinkStateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { Terminal } from 'xterm';
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(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);
}
}
1 change: 1 addition & 0 deletions addons/xterm-addon-webgl/src/GlyphRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
106 changes: 93 additions & 13 deletions addons/xterm-addon-webgl/src/RectangleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -229,9 +242,76 @@ export class RectangleRenderer extends Disposable {
}
}
vertices.count = rectangleCount;

this._updateCursor(model);
}

private _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: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
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) {
Expand Down
8 changes: 8 additions & 0 deletions addons/xterm-addon-webgl/src/RenderModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ export class RenderModel implements IRenderModel {
public cells: Uint32Array;
public lineLengths: Uint32Array;
public selection: ISelectionRenderModel;
public cursor?: {
x: number;
y: number;
width: number;
style: string;
cursorWidth: number;
dpr: number;
};

constructor() {
this.cells = new Uint32Array(0);
Expand Down
8 changes: 8 additions & 0 deletions addons/xterm-addon-webgl/src/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ export interface IRenderModel {
cells: Uint32Array;
lineLengths: Uint32Array;
selection: ISelectionRenderModel;
cursor?: {
x: number;
y: number;
width: number;
style: string;
cursorWidth: number;
dpr: number;
};
}

export interface IWebGL2RenderingContext extends WebGLRenderingContext {
Expand Down
Loading

0 comments on commit 064fe0c

Please sign in to comment.