@xterm/addon-webgl 0.17.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +41 -0
- package/lib/addon-webgl.js +2 -0
- package/lib/addon-webgl.js.map +1 -0
- package/package.json +28 -0
- package/src/GlyphRenderer.ts +381 -0
- package/src/RectangleRenderer.ts +382 -0
- package/src/RenderModel.ts +41 -0
- package/src/TypedArray.ts +32 -0
- package/src/Types.d.ts +33 -0
- package/src/WebglAddon.ts +93 -0
- package/src/WebglRenderer.ts +646 -0
- package/src/WebglUtils.ts +63 -0
- package/src/renderLayer/BaseRenderLayer.ts +220 -0
- package/src/renderLayer/LinkRenderLayer.ts +82 -0
- package/src/renderLayer/Types.ts +55 -0
- package/typings/addon-webgl.d.ts +48 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { addDisposableDomListener } from 'browser/Lifecycle';
|
|
7
|
+
import { ITerminal } from 'browser/Types';
|
|
8
|
+
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
|
9
|
+
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
|
|
10
|
+
import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager';
|
|
11
|
+
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
|
|
12
|
+
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
|
13
|
+
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
|
|
14
|
+
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
|
15
|
+
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
|
16
|
+
import { Disposable, MutableDisposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
|
|
17
|
+
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
|
18
|
+
import { AttributeData } from 'common/buffer/AttributeData';
|
|
19
|
+
import { CellData } from 'common/buffer/CellData';
|
|
20
|
+
import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
|
21
|
+
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
|
22
|
+
import { Terminal } from '@xterm/xterm';
|
|
23
|
+
import { GlyphRenderer } from './GlyphRenderer';
|
|
24
|
+
import { RectangleRenderer } from './RectangleRenderer';
|
|
25
|
+
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL, RenderModel } from './RenderModel';
|
|
26
|
+
import { IWebGL2RenderingContext } from './Types';
|
|
27
|
+
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
|
28
|
+
import { IRenderLayer } from './renderLayer/Types';
|
|
29
|
+
|
|
30
|
+
export class WebglRenderer extends Disposable implements IRenderer {
|
|
31
|
+
private _renderLayers: IRenderLayer[];
|
|
32
|
+
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
|
|
33
|
+
private _charAtlasDisposable = this.register(new MutableDisposable());
|
|
34
|
+
private _charAtlas: ITextureAtlas | undefined;
|
|
35
|
+
private _devicePixelRatio: number;
|
|
36
|
+
|
|
37
|
+
private _model: RenderModel = new RenderModel();
|
|
38
|
+
private _workCell: CellData = new CellData();
|
|
39
|
+
private _cellColorResolver: CellColorResolver;
|
|
40
|
+
|
|
41
|
+
private _canvas: HTMLCanvasElement;
|
|
42
|
+
private _gl: IWebGL2RenderingContext;
|
|
43
|
+
private _rectangleRenderer: MutableDisposable<RectangleRenderer> = this.register(new MutableDisposable());
|
|
44
|
+
private _glyphRenderer: MutableDisposable<GlyphRenderer> = this.register(new MutableDisposable());
|
|
45
|
+
|
|
46
|
+
public readonly dimensions: IRenderDimensions;
|
|
47
|
+
|
|
48
|
+
private _core: ITerminal;
|
|
49
|
+
private _isAttached: boolean;
|
|
50
|
+
private _contextRestorationTimeout: number | undefined;
|
|
51
|
+
|
|
52
|
+
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
|
53
|
+
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
|
54
|
+
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
|
55
|
+
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
|
56
|
+
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
|
57
|
+
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
|
|
58
|
+
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
|
|
59
|
+
public readonly onRequestRedraw = this._onRequestRedraw.event;
|
|
60
|
+
private readonly _onContextLoss = this.register(new EventEmitter<void>());
|
|
61
|
+
public readonly onContextLoss = this._onContextLoss.event;
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private _terminal: Terminal,
|
|
65
|
+
private readonly _characterJoinerService: ICharacterJoinerService,
|
|
66
|
+
private readonly _charSizeService: ICharSizeService,
|
|
67
|
+
private readonly _coreBrowserService: ICoreBrowserService,
|
|
68
|
+
private readonly _coreService: ICoreService,
|
|
69
|
+
private readonly _decorationService: IDecorationService,
|
|
70
|
+
private readonly _optionsService: IOptionsService,
|
|
71
|
+
private readonly _themeService: IThemeService,
|
|
72
|
+
preserveDrawingBuffer?: boolean
|
|
73
|
+
) {
|
|
74
|
+
super();
|
|
75
|
+
|
|
76
|
+
this.register(this._themeService.onChangeColors(() => this._handleColorChange()));
|
|
77
|
+
|
|
78
|
+
this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
|
|
79
|
+
|
|
80
|
+
this._core = (this._terminal as any)._core;
|
|
81
|
+
|
|
82
|
+
this._renderLayers = [
|
|
83
|
+
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService)
|
|
84
|
+
];
|
|
85
|
+
this.dimensions = createRenderDimensions();
|
|
86
|
+
this._devicePixelRatio = this._coreBrowserService.dpr;
|
|
87
|
+
this._updateDimensions();
|
|
88
|
+
this._updateCursorBlink();
|
|
89
|
+
this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
|
90
|
+
|
|
91
|
+
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
|
|
92
|
+
|
|
93
|
+
const contextAttributes = {
|
|
94
|
+
antialias: false,
|
|
95
|
+
depth: false,
|
|
96
|
+
preserveDrawingBuffer
|
|
97
|
+
};
|
|
98
|
+
this._gl = this._canvas.getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
|
99
|
+
if (!this._gl) {
|
|
100
|
+
throw new Error('WebGL2 not supported ' + this._gl);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => {
|
|
104
|
+
console.log('webglcontextlost event received');
|
|
105
|
+
// Prevent the default behavior in order to enable WebGL context restoration.
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
|
|
108
|
+
// If not, dispatch the onContextLoss notification to observers.
|
|
109
|
+
this._contextRestorationTimeout = setTimeout(() => {
|
|
110
|
+
this._contextRestorationTimeout = undefined;
|
|
111
|
+
console.warn('webgl context not restored; firing onContextLoss');
|
|
112
|
+
this._onContextLoss.fire(e);
|
|
113
|
+
}, 3000 /* ms */);
|
|
114
|
+
}));
|
|
115
|
+
this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => {
|
|
116
|
+
console.warn('webglcontextrestored event received');
|
|
117
|
+
clearTimeout(this._contextRestorationTimeout);
|
|
118
|
+
this._contextRestorationTimeout = undefined;
|
|
119
|
+
// The texture atlas and glyph renderer must be fully reinitialized
|
|
120
|
+
// because their contents have been lost.
|
|
121
|
+
removeTerminalFromCache(this._terminal);
|
|
122
|
+
this._initializeWebGLState();
|
|
123
|
+
this._requestRedrawViewport();
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
|
127
|
+
|
|
128
|
+
this._core.screenElement!.appendChild(this._canvas);
|
|
129
|
+
|
|
130
|
+
[this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
|
|
131
|
+
|
|
132
|
+
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
|
|
133
|
+
|
|
134
|
+
this.register(toDisposable(() => {
|
|
135
|
+
for (const l of this._renderLayers) {
|
|
136
|
+
l.dispose();
|
|
137
|
+
}
|
|
138
|
+
this._canvas.parentElement?.removeChild(this._canvas);
|
|
139
|
+
removeTerminalFromCache(this._terminal);
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public get textureAtlas(): HTMLCanvasElement | undefined {
|
|
144
|
+
return this._charAtlas?.pages[0].canvas;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private _handleColorChange(): void {
|
|
148
|
+
this._refreshCharAtlas();
|
|
149
|
+
|
|
150
|
+
// Force a full refresh
|
|
151
|
+
this._clearModel(true);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public handleDevicePixelRatioChange(): void {
|
|
155
|
+
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
|
156
|
+
// and the terminal needs to refreshed
|
|
157
|
+
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
|
|
158
|
+
this._devicePixelRatio = this._coreBrowserService.dpr;
|
|
159
|
+
this.handleResize(this._terminal.cols, this._terminal.rows);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public handleResize(cols: number, rows: number): void {
|
|
164
|
+
// Update character and canvas dimensions
|
|
165
|
+
this._updateDimensions();
|
|
166
|
+
|
|
167
|
+
this._model.resize(this._terminal.cols, this._terminal.rows);
|
|
168
|
+
|
|
169
|
+
// Resize all render layers
|
|
170
|
+
for (const l of this._renderLayers) {
|
|
171
|
+
l.resize(this._terminal, this.dimensions);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Resize the canvas
|
|
175
|
+
this._canvas.width = this.dimensions.device.canvas.width;
|
|
176
|
+
this._canvas.height = this.dimensions.device.canvas.height;
|
|
177
|
+
this._canvas.style.width = `${this.dimensions.css.canvas.width}px`;
|
|
178
|
+
this._canvas.style.height = `${this.dimensions.css.canvas.height}px`;
|
|
179
|
+
|
|
180
|
+
// Resize the screen
|
|
181
|
+
this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`;
|
|
182
|
+
this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`;
|
|
183
|
+
|
|
184
|
+
this._rectangleRenderer.value?.setDimensions(this.dimensions);
|
|
185
|
+
this._rectangleRenderer.value?.handleResize();
|
|
186
|
+
this._glyphRenderer.value?.setDimensions(this.dimensions);
|
|
187
|
+
this._glyphRenderer.value?.handleResize();
|
|
188
|
+
|
|
189
|
+
this._refreshCharAtlas();
|
|
190
|
+
|
|
191
|
+
// Force a full refresh. Resizing `_glyphRenderer` should clear it already,
|
|
192
|
+
// so there is no need to clear it again here.
|
|
193
|
+
this._clearModel(false);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public handleCharSizeChanged(): void {
|
|
197
|
+
this.handleResize(this._terminal.cols, this._terminal.rows);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public handleBlur(): void {
|
|
201
|
+
for (const l of this._renderLayers) {
|
|
202
|
+
l.handleBlur(this._terminal);
|
|
203
|
+
}
|
|
204
|
+
this._cursorBlinkStateManager.value?.pause();
|
|
205
|
+
// Request a redraw for active/inactive selection background
|
|
206
|
+
this._requestRedrawViewport();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public handleFocus(): void {
|
|
210
|
+
for (const l of this._renderLayers) {
|
|
211
|
+
l.handleFocus(this._terminal);
|
|
212
|
+
}
|
|
213
|
+
this._cursorBlinkStateManager.value?.resume();
|
|
214
|
+
// Request a redraw for active/inactive selection background
|
|
215
|
+
this._requestRedrawViewport();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
|
219
|
+
for (const l of this._renderLayers) {
|
|
220
|
+
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
|
|
221
|
+
}
|
|
222
|
+
this._model.selection.update(this._terminal, start, end, columnSelectMode);
|
|
223
|
+
this._requestRedrawViewport();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public handleCursorMove(): void {
|
|
227
|
+
for (const l of this._renderLayers) {
|
|
228
|
+
l.handleCursorMove(this._terminal);
|
|
229
|
+
}
|
|
230
|
+
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private _handleOptionsChanged(): void {
|
|
234
|
+
this._updateDimensions();
|
|
235
|
+
this._refreshCharAtlas();
|
|
236
|
+
this._updateCursorBlink();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Initializes members dependent on WebGL context state.
|
|
241
|
+
*/
|
|
242
|
+
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
|
|
243
|
+
this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService);
|
|
244
|
+
this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions);
|
|
245
|
+
|
|
246
|
+
// Update dimensions and acquire char atlas
|
|
247
|
+
this.handleCharSizeChanged();
|
|
248
|
+
|
|
249
|
+
return [this._rectangleRenderer.value, this._glyphRenderer.value];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Refreshes the char atlas, aquiring a new one if necessary.
|
|
254
|
+
*/
|
|
255
|
+
private _refreshCharAtlas(): void {
|
|
256
|
+
if (this.dimensions.device.char.width <= 0 && this.dimensions.device.char.height <= 0) {
|
|
257
|
+
// Mark as not attached so char atlas gets refreshed on next render
|
|
258
|
+
this._isAttached = false;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const atlas = acquireTextureAtlas(
|
|
263
|
+
this._terminal,
|
|
264
|
+
this._optionsService.rawOptions,
|
|
265
|
+
this._themeService.colors,
|
|
266
|
+
this.dimensions.device.cell.width,
|
|
267
|
+
this.dimensions.device.cell.height,
|
|
268
|
+
this.dimensions.device.char.width,
|
|
269
|
+
this.dimensions.device.char.height,
|
|
270
|
+
this._coreBrowserService.dpr
|
|
271
|
+
);
|
|
272
|
+
if (this._charAtlas !== atlas) {
|
|
273
|
+
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
|
274
|
+
this._charAtlasDisposable.value = getDisposeArrayDisposable([
|
|
275
|
+
forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
|
|
276
|
+
forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
this._charAtlas = atlas;
|
|
280
|
+
this._charAtlas.warmUp();
|
|
281
|
+
this._glyphRenderer.value?.setAtlas(this._charAtlas);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Clear the model.
|
|
286
|
+
* @param clearGlyphRenderer Whether to also clear the glyph renderer. This
|
|
287
|
+
* should be true generally to make sure it is in the same state as the model.
|
|
288
|
+
*/
|
|
289
|
+
private _clearModel(clearGlyphRenderer: boolean): void {
|
|
290
|
+
this._model.clear();
|
|
291
|
+
if (clearGlyphRenderer) {
|
|
292
|
+
this._glyphRenderer.value?.clear();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public clearTextureAtlas(): void {
|
|
297
|
+
this._charAtlas?.clearTexture();
|
|
298
|
+
this._clearModel(true);
|
|
299
|
+
this._requestRedrawViewport();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
public clear(): void {
|
|
303
|
+
this._clearModel(true);
|
|
304
|
+
for (const l of this._renderLayers) {
|
|
305
|
+
l.reset(this._terminal);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
|
|
309
|
+
this._updateCursorBlink();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
|
313
|
+
return -1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public deregisterCharacterJoiner(joinerId: number): boolean {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public renderRows(start: number, end: number): void {
|
|
321
|
+
if (!this._isAttached) {
|
|
322
|
+
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
|
|
323
|
+
this._updateDimensions();
|
|
324
|
+
this._refreshCharAtlas();
|
|
325
|
+
this._isAttached = true;
|
|
326
|
+
} else {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Update render layers
|
|
332
|
+
for (const l of this._renderLayers) {
|
|
333
|
+
l.handleGridChanged(this._terminal, start, end);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!this._glyphRenderer.value || !this._rectangleRenderer.value) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Tell renderer the frame is beginning
|
|
341
|
+
// upon a model clear also refresh the full viewport model
|
|
342
|
+
// (also triggered by an atlas page merge, part of #4480)
|
|
343
|
+
if (this._glyphRenderer.value.beginFrame()) {
|
|
344
|
+
this._clearModel(true);
|
|
345
|
+
this._updateModel(0, this._terminal.rows - 1);
|
|
346
|
+
} else {
|
|
347
|
+
// just update changed lines to draw
|
|
348
|
+
this._updateModel(start, end);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Render
|
|
352
|
+
this._rectangleRenderer.value.renderBackgrounds();
|
|
353
|
+
this._glyphRenderer.value.render(this._model);
|
|
354
|
+
if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible) {
|
|
355
|
+
this._rectangleRenderer.value.renderCursor();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private _updateCursorBlink(): void {
|
|
360
|
+
if (this._terminal.options.cursorBlink) {
|
|
361
|
+
this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => {
|
|
362
|
+
this._requestRedrawCursor();
|
|
363
|
+
}, this._coreBrowserService);
|
|
364
|
+
} else {
|
|
365
|
+
this._cursorBlinkStateManager.clear();
|
|
366
|
+
}
|
|
367
|
+
// Request a refresh from the terminal as management of rendering is being
|
|
368
|
+
// moved back to the terminal
|
|
369
|
+
this._requestRedrawCursor();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private _updateModel(start: number, end: number): void {
|
|
373
|
+
const terminal = this._core;
|
|
374
|
+
let cell: ICellData = this._workCell;
|
|
375
|
+
|
|
376
|
+
// Declare variable ahead of time to avoid garbage collection
|
|
377
|
+
let lastBg: number;
|
|
378
|
+
let y: number;
|
|
379
|
+
let row: number;
|
|
380
|
+
let line: IBufferLine;
|
|
381
|
+
let joinedRanges: [number, number][];
|
|
382
|
+
let isJoined: boolean;
|
|
383
|
+
let lastCharX: number;
|
|
384
|
+
let range: [number, number];
|
|
385
|
+
let chars: string;
|
|
386
|
+
let code: number;
|
|
387
|
+
let i: number;
|
|
388
|
+
let x: number;
|
|
389
|
+
let j: number;
|
|
390
|
+
start = clamp(start, terminal.rows - 1, 0);
|
|
391
|
+
end = clamp(end, terminal.rows - 1, 0);
|
|
392
|
+
|
|
393
|
+
const cursorY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;
|
|
394
|
+
// in case cursor.x == cols adjust visual cursor to cols - 1
|
|
395
|
+
const cursorX = Math.min(this._terminal.buffer.active.cursorX, terminal.cols - 1);
|
|
396
|
+
let lastCursorX = -1;
|
|
397
|
+
const isCursorVisible =
|
|
398
|
+
this._coreService.isCursorInitialized &&
|
|
399
|
+
!this._coreService.isCursorHidden &&
|
|
400
|
+
(!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible);
|
|
401
|
+
this._model.cursor = undefined;
|
|
402
|
+
let modelUpdated = false;
|
|
403
|
+
|
|
404
|
+
for (y = start; y <= end; y++) {
|
|
405
|
+
row = y + terminal.buffer.ydisp;
|
|
406
|
+
line = terminal.buffer.lines.get(row)!;
|
|
407
|
+
this._model.lineLengths[y] = 0;
|
|
408
|
+
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
|
409
|
+
for (x = 0; x < terminal.cols; x++) {
|
|
410
|
+
lastBg = this._cellColorResolver.result.bg;
|
|
411
|
+
line.loadCell(x, cell);
|
|
412
|
+
|
|
413
|
+
if (x === 0) {
|
|
414
|
+
lastBg = this._cellColorResolver.result.bg;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// If true, indicates that the current character(s) to draw were joined.
|
|
418
|
+
isJoined = false;
|
|
419
|
+
lastCharX = x;
|
|
420
|
+
|
|
421
|
+
// Process any joined character ranges as needed. Because of how the
|
|
422
|
+
// ranges are produced, we know that they are valid for the characters
|
|
423
|
+
// and attributes of our input.
|
|
424
|
+
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
|
425
|
+
isJoined = true;
|
|
426
|
+
range = joinedRanges.shift()!;
|
|
427
|
+
|
|
428
|
+
// We already know the exact start and end column of the joined range,
|
|
429
|
+
// so we get the string and width representing it directly.
|
|
430
|
+
cell = new JoinedCellData(
|
|
431
|
+
cell,
|
|
432
|
+
line!.translateToString(true, range[0], range[1]),
|
|
433
|
+
range[1] - range[0]
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// Skip over the cells occupied by this range in the loop
|
|
437
|
+
lastCharX = range[1] - 1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
chars = cell.getChars();
|
|
441
|
+
code = cell.getCode();
|
|
442
|
+
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
|
443
|
+
|
|
444
|
+
// Load colors/resolve overrides into work colors
|
|
445
|
+
this._cellColorResolver.resolve(cell, x, row);
|
|
446
|
+
|
|
447
|
+
// Override colors for cursor cell
|
|
448
|
+
if (isCursorVisible && row === cursorY) {
|
|
449
|
+
if (x === cursorX) {
|
|
450
|
+
this._model.cursor = {
|
|
451
|
+
x: cursorX,
|
|
452
|
+
y: this._terminal.buffer.active.cursorY,
|
|
453
|
+
width: cell.getWidth(),
|
|
454
|
+
style: this._coreBrowserService.isFocused ?
|
|
455
|
+
(terminal.options.cursorStyle || 'block') : terminal.options.cursorInactiveStyle,
|
|
456
|
+
cursorWidth: terminal.options.cursorWidth,
|
|
457
|
+
dpr: this._devicePixelRatio
|
|
458
|
+
};
|
|
459
|
+
lastCursorX = cursorX + cell.getWidth() - 1;
|
|
460
|
+
}
|
|
461
|
+
if (x >= cursorX && x <= lastCursorX &&
|
|
462
|
+
((this._coreBrowserService.isFocused &&
|
|
463
|
+
(terminal.options.cursorStyle || 'block') === 'block') ||
|
|
464
|
+
(this._coreBrowserService.isFocused === false &&
|
|
465
|
+
terminal.options.cursorInactiveStyle === 'block'))) {
|
|
466
|
+
this._cellColorResolver.result.fg =
|
|
467
|
+
Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK);
|
|
468
|
+
this._cellColorResolver.result.bg =
|
|
469
|
+
Attributes.CM_RGB | (this._themeService.colors.cursor.rgba >> 8 & Attributes.RGB_MASK);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (code !== NULL_CELL_CODE) {
|
|
474
|
+
this._model.lineLengths[y] = x + 1;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Nothing has changed, no updates needed
|
|
478
|
+
if (this._model.cells[i] === code &&
|
|
479
|
+
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
|
|
480
|
+
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
|
|
481
|
+
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
modelUpdated = true;
|
|
486
|
+
|
|
487
|
+
// Flag combined chars with a bit mask so they're easily identifiable
|
|
488
|
+
if (chars.length > 1) {
|
|
489
|
+
code |= COMBINED_CHAR_BIT_MASK;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Cache the results in the model
|
|
493
|
+
this._model.cells[i] = code;
|
|
494
|
+
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
|
495
|
+
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
|
496
|
+
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
|
497
|
+
|
|
498
|
+
this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
|
|
499
|
+
|
|
500
|
+
if (isJoined) {
|
|
501
|
+
// Restore work cell
|
|
502
|
+
cell = this._workCell;
|
|
503
|
+
|
|
504
|
+
// Null out non-first cells
|
|
505
|
+
for (x++; x < lastCharX; x++) {
|
|
506
|
+
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
|
507
|
+
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
|
|
508
|
+
this._model.cells[j] = NULL_CELL_CODE;
|
|
509
|
+
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
|
510
|
+
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
|
511
|
+
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (modelUpdated) {
|
|
517
|
+
this._rectangleRenderer.value!.updateBackgrounds(this._model);
|
|
518
|
+
}
|
|
519
|
+
this._rectangleRenderer.value!.updateCursor(this._model);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Recalculates the character and canvas dimensions.
|
|
524
|
+
*/
|
|
525
|
+
private _updateDimensions(): void {
|
|
526
|
+
// Perform a new measure if the CharMeasure dimensions are not yet available
|
|
527
|
+
if (!this._charSizeService.width || !this._charSizeService.height) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Calculate the device character width. Width is floored as it must be drawn to an integer grid
|
|
532
|
+
// in order for the char atlas glyphs to not be blurry.
|
|
533
|
+
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * this._devicePixelRatio);
|
|
534
|
+
|
|
535
|
+
// Calculate the device character height. Height is ceiled in case devicePixelRatio is a
|
|
536
|
+
// floating point number in order to ensure there is enough space to draw the character to the
|
|
537
|
+
// cell.
|
|
538
|
+
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
|
|
539
|
+
|
|
540
|
+
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
|
|
541
|
+
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
|
|
542
|
+
// will always be larger than device char height.
|
|
543
|
+
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
|
544
|
+
|
|
545
|
+
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
|
|
546
|
+
// correctly within the cell.
|
|
547
|
+
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
|
|
548
|
+
|
|
549
|
+
// Calculate the device cell width, taking the letterSpacing into account.
|
|
550
|
+
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
|
551
|
+
|
|
552
|
+
// Calculate the x offset with a cell that text should draw from in order for it to be centered
|
|
553
|
+
// correctly within the cell.
|
|
554
|
+
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
|
555
|
+
|
|
556
|
+
// Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in
|
|
557
|
+
// the canvas
|
|
558
|
+
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
|
|
559
|
+
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
|
|
560
|
+
|
|
561
|
+
// The the size of the canvas on the page. It's important that this rounds to nearest integer
|
|
562
|
+
// and not ceils as browsers often have floating point precision issues where
|
|
563
|
+
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
|
564
|
+
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
|
|
565
|
+
// large for the canvas element size.
|
|
566
|
+
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
|
|
567
|
+
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
|
|
568
|
+
|
|
569
|
+
// Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
|
|
570
|
+
// device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
|
|
571
|
+
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
|
|
572
|
+
// size on the canvas can differ.
|
|
573
|
+
this.dimensions.css.cell.height = this.dimensions.device.cell.height / this._devicePixelRatio;
|
|
574
|
+
this.dimensions.css.cell.width = this.dimensions.device.cell.width / this._devicePixelRatio;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
|
578
|
+
if (this._canvas.width === width && this._canvas.height === height) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// While the actual canvas size has changed, keep device canvas dimensions as the value before
|
|
582
|
+
// the change as it's an exact multiple of the cell sizes.
|
|
583
|
+
this._canvas.width = width;
|
|
584
|
+
this._canvas.height = height;
|
|
585
|
+
this._requestRedrawViewport();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private _requestRedrawViewport(): void {
|
|
589
|
+
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private _requestRedrawCursor(): void {
|
|
593
|
+
const cursorY = this._terminal.buffer.active.cursorY;
|
|
594
|
+
this._onRequestRedraw.fire({ start: cursorY, end: cursorY });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// TODO: Share impl with core
|
|
599
|
+
export class JoinedCellData extends AttributeData implements ICellData {
|
|
600
|
+
private _width: number;
|
|
601
|
+
// .content carries no meaning for joined CellData, simply nullify it
|
|
602
|
+
// thus we have to overload all other .content accessors
|
|
603
|
+
public content: number = 0;
|
|
604
|
+
public fg: number;
|
|
605
|
+
public bg: number;
|
|
606
|
+
public combinedData: string = '';
|
|
607
|
+
|
|
608
|
+
constructor(firstCell: ICellData, chars: string, width: number) {
|
|
609
|
+
super();
|
|
610
|
+
this.fg = firstCell.fg;
|
|
611
|
+
this.bg = firstCell.bg;
|
|
612
|
+
this.combinedData = chars;
|
|
613
|
+
this._width = width;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
public isCombined(): number {
|
|
617
|
+
// always mark joined cell data as combined
|
|
618
|
+
return Content.IS_COMBINED_MASK;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
public getWidth(): number {
|
|
622
|
+
return this._width;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
public getChars(): string {
|
|
626
|
+
return this.combinedData;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
public getCode(): number {
|
|
630
|
+
// code always gets the highest possible fake codepoint (read as -1)
|
|
631
|
+
// this is needed as code is used by caches as identifier
|
|
632
|
+
return 0x1FFFFF;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
public setFromCharData(value: CharData): void {
|
|
636
|
+
throw new Error('not implemented');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
public getAsCharData(): CharData {
|
|
640
|
+
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function clamp(value: number, max: number, min: number = 0): number {
|
|
645
|
+
return Math.max(Math.min(value, max), min);
|
|
646
|
+
}
|