Skip to content

Commit 02a613b

Browse files
committed
Render the cursor in the WebGL canvas
1 parent ac923e5 commit 02a613b

File tree

8 files changed

+334
-438
lines changed

8 files changed

+334
-438
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { Terminal } from 'xterm';
7+
import { ICoreBrowserService } from 'browser/services/Services';
8+
9+
/**
10+
* The time between cursor blinks.
11+
*/
12+
const BLINK_INTERVAL = 600;
13+
14+
export class CursorBlinkStateManager {
15+
public isCursorVisible: boolean;
16+
17+
private _animationFrame: number | undefined;
18+
private _blinkStartTimeout: number | undefined;
19+
private _blinkInterval: number | undefined;
20+
21+
/**
22+
* The time at which the animation frame was restarted, this is used on the
23+
* next render to restart the timers so they don't need to restart the timers
24+
* multiple times over a short period.
25+
*/
26+
private _animationTimeRestarted: number | undefined;
27+
28+
constructor(
29+
private _renderCallback: () => void,
30+
private _coreBrowserService: ICoreBrowserService
31+
) {
32+
this.isCursorVisible = true;
33+
if (this._coreBrowserService.isFocused) {
34+
this._restartInterval();
35+
}
36+
}
37+
38+
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
39+
40+
public dispose(): void {
41+
if (this._blinkInterval) {
42+
this._coreBrowserService.window.clearInterval(this._blinkInterval);
43+
this._blinkInterval = undefined;
44+
}
45+
if (this._blinkStartTimeout) {
46+
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
47+
this._blinkStartTimeout = undefined;
48+
}
49+
if (this._animationFrame) {
50+
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
51+
this._animationFrame = undefined;
52+
}
53+
}
54+
55+
public restartBlinkAnimation(terminal: Terminal): void {
56+
if (this.isPaused) {
57+
return;
58+
}
59+
// Save a timestamp so that the restart can be done on the next interval
60+
this._animationTimeRestarted = Date.now();
61+
// Force a cursor render to ensure it's visible and in the correct position
62+
this.isCursorVisible = true;
63+
if (!this._animationFrame) {
64+
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
65+
this._renderCallback();
66+
this._animationFrame = undefined;
67+
});
68+
}
69+
}
70+
71+
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
72+
// Clear any existing interval
73+
if (this._blinkInterval) {
74+
this._coreBrowserService.window.clearInterval(this._blinkInterval);
75+
this._blinkInterval = undefined;
76+
}
77+
78+
// Setup the initial timeout which will hide the cursor, this is done before
79+
// the regular interval is setup in order to support restarting the blink
80+
// animation in a lightweight way (without thrashing clearInterval and
81+
// setInterval).
82+
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
83+
// Check if another animation restart was requested while this was being
84+
// started
85+
if (this._animationTimeRestarted) {
86+
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
87+
this._animationTimeRestarted = undefined;
88+
if (time > 0) {
89+
this._restartInterval(time);
90+
return;
91+
}
92+
}
93+
94+
// Hide the cursor
95+
this.isCursorVisible = false;
96+
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
97+
this._renderCallback();
98+
this._animationFrame = undefined;
99+
});
100+
101+
// Setup the blink interval
102+
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
103+
// Adjust the animation time if it was restarted
104+
if (this._animationTimeRestarted) {
105+
// calc time diff
106+
// Make restart interval do a setTimeout initially?
107+
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
108+
this._animationTimeRestarted = undefined;
109+
this._restartInterval(time);
110+
return;
111+
}
112+
113+
// Invert visibility and render
114+
this.isCursorVisible = !this.isCursorVisible;
115+
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
116+
this._renderCallback();
117+
this._animationFrame = undefined;
118+
});
119+
}, BLINK_INTERVAL);
120+
}, timeToStart);
121+
}
122+
123+
public pause(): void {
124+
this.isCursorVisible = true;
125+
if (this._blinkInterval) {
126+
this._coreBrowserService.window.clearInterval(this._blinkInterval);
127+
this._blinkInterval = undefined;
128+
}
129+
if (this._blinkStartTimeout) {
130+
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
131+
this._blinkStartTimeout = undefined;
132+
}
133+
if (this._animationFrame) {
134+
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
135+
this._animationFrame = undefined;
136+
}
137+
}
138+
139+
public resume(terminal: Terminal): void {
140+
// Clear out any existing timers just in case
141+
this.pause();
142+
143+
this._animationTimeRestarted = undefined;
144+
this._restartInterval();
145+
this.restartBlinkAnimation(terminal);
146+
}
147+
}

addons/xterm-addon-webgl/src/GlyphRenderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ export class GlyphRenderer extends Disposable {
308308

309309
public handleResize(): void {
310310
const gl = this._gl;
311+
gl.useProgram(this._program);
311312
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
312313
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
313314
this.clear();

addons/xterm-addon-webgl/src/RectangleRenderer.ts

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,21 @@ void main() {
5050
outColor = v_color;
5151
}`;
5252

53-
interface IVertices {
54-
attributes: Float32Array;
55-
count: number;
56-
}
57-
5853
const INDICES_PER_RECTANGLE = 8;
5954
const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEMENT;
6055

6156
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
6257

58+
class Vertices {
59+
attributes: Float32Array;
60+
count: number;
61+
62+
constructor() {
63+
this.attributes = new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY);
64+
this.count = 0;
65+
}
66+
}
67+
6368
// Work variables to avoid garbage collection
6469
let $rgba = 0;
6570
let $isDefault = false;
@@ -77,11 +82,10 @@ export class RectangleRenderer extends Disposable {
7782
private _attributesBuffer: WebGLBuffer;
7883
private _projectionLocation: WebGLUniformLocation;
7984
private _bgFloat!: Float32Array;
85+
private _cursorFloat!: Float32Array;
8086

81-
private _vertices: IVertices = {
82-
count: 0,
83-
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY)
84-
};
87+
private _vertices: Vertices = new Vertices();
88+
private _verticesCursor: Vertices = new Vertices();
8589

8690
constructor(
8791
private _terminal: Terminal,
@@ -142,7 +146,15 @@ export class RectangleRenderer extends Disposable {
142146
}));
143147
}
144148

145-
public render(): void {
149+
public renderBackgrounds(): void {
150+
this._renderVertices(this._vertices);
151+
}
152+
153+
public renderCursor(): void {
154+
this._renderVertices(this._verticesCursor);
155+
}
156+
157+
private _renderVertices(vertices: Vertices): void {
146158
const gl = this._gl;
147159

148160
gl.useProgram(this._program);
@@ -153,8 +165,8 @@ export class RectangleRenderer extends Disposable {
153165

154166
// Bind attributes buffer and draw
155167
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
156-
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
157-
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count);
168+
gl.bufferData(gl.ARRAY_BUFFER, vertices.attributes, gl.DYNAMIC_DRAW);
169+
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, vertices.count);
158170
}
159171

160172
public handleResize(): void {
@@ -167,6 +179,7 @@ export class RectangleRenderer extends Disposable {
167179

168180
private _updateCachedColors(colors: ReadonlyColorSet): void {
169181
this._bgFloat = this._colorToFloat32Array(colors.background);
182+
this._cursorFloat = this._colorToFloat32Array(colors.cursor);
170183
}
171184

172185
private _updateViewportRectangle(): void {
@@ -229,9 +242,76 @@ export class RectangleRenderer extends Disposable {
229242
}
230243
}
231244
vertices.count = rectangleCount;
245+
246+
this._updateCursor(model);
247+
}
248+
249+
private _updateCursor(model: IRenderModel): void {
250+
const vertices = this._verticesCursor;
251+
const cursor = model.cursor;
252+
if (!cursor || cursor.style === 'block') {
253+
vertices.count = 0;
254+
return;
255+
}
256+
257+
let offset: number;
258+
let rectangleCount = 0;
259+
260+
if (cursor.style === 'bar' || cursor.style === 'blur') {
261+
// Left edge
262+
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
263+
this._addRectangleFloat(
264+
vertices.attributes,
265+
offset,
266+
cursor.x * this._dimensions.device.cell.width,
267+
cursor.y * this._dimensions.device.cell.height,
268+
cursor.style === 'bar' ? cursor.dpr * cursor.cursorWidth : cursor.dpr,
269+
this._dimensions.device.cell.height,
270+
this._cursorFloat
271+
);
272+
}
273+
if (cursor.style === 'underline' || cursor.style === 'blur') {
274+
// Bottom edge
275+
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
276+
this._addRectangleFloat(
277+
vertices.attributes,
278+
offset,
279+
cursor.x * this._dimensions.device.cell.width,
280+
(cursor.y + 1) * this._dimensions.device.cell.height - cursor.dpr,
281+
cursor.width * this._dimensions.device.cell.width,
282+
cursor.dpr,
283+
this._cursorFloat
284+
);
285+
}
286+
if (cursor.style === 'blur') {
287+
// Top edge
288+
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
289+
this._addRectangleFloat(
290+
vertices.attributes,
291+
offset,
292+
cursor.x * this._dimensions.device.cell.width,
293+
cursor.y * this._dimensions.device.cell.height,
294+
cursor.width * this._dimensions.device.cell.width,
295+
cursor.dpr,
296+
this._cursorFloat
297+
);
298+
// Right edge
299+
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
300+
this._addRectangleFloat(
301+
vertices.attributes,
302+
offset,
303+
(cursor.x + cursor.width) * this._dimensions.device.cell.width - cursor.dpr,
304+
cursor.y * this._dimensions.device.cell.height,
305+
cursor.dpr,
306+
this._dimensions.device.cell.height,
307+
this._cursorFloat
308+
);
309+
}
310+
311+
vertices.count = rectangleCount;
232312
}
233313

234-
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
314+
private _updateRectangle(vertices: Vertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
235315
$isDefault = false;
236316
if (fg & FgFlags.INVERSE) {
237317
switch (fg & Attributes.CM_MASK) {

addons/xterm-addon-webgl/src/RenderModel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export class RenderModel implements IRenderModel {
1818
public cells: Uint32Array;
1919
public lineLengths: Uint32Array;
2020
public selection: ISelectionRenderModel;
21+
public cursor?: {
22+
x: number;
23+
y: number;
24+
width: number;
25+
style: string;
26+
cursorWidth: number;
27+
dpr: number;
28+
};
2129

2230
constructor() {
2331
this.cells = new Uint32Array(0);

addons/xterm-addon-webgl/src/Types.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export interface IRenderModel {
99
cells: Uint32Array;
1010
lineLengths: Uint32Array;
1111
selection: ISelectionRenderModel;
12+
cursor?: {
13+
x: number;
14+
y: number;
15+
width: number;
16+
style: string;
17+
cursorWidth: number;
18+
dpr: number;
19+
};
1220
}
1321

1422
export interface IWebGL2RenderingContext extends WebGLRenderingContext {

0 commit comments

Comments
 (0)