@xterm/addon-webgl 0.20.0-beta.23 → 0.20.0-beta.231

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.
@@ -14,8 +14,8 @@ import { IdleTaskQueue } from 'common/TaskQueue';
14
14
  import { IColor } from 'common/Types';
15
15
  import { AttributeData } from 'common/buffer/AttributeData';
16
16
  import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
17
- import { IUnicodeService } from 'common/services/Services';
18
- import { Emitter } from 'vs/base/common/event';
17
+ import { ILogService, IUnicodeService } from 'common/services/Services';
18
+ import { Emitter } from 'common/Event';
19
19
 
20
20
  /**
21
21
  * A shared object which is used to draw nothing for a particular cell.
@@ -88,7 +88,8 @@ export class TextureAtlas implements ITextureAtlas {
88
88
  constructor(
89
89
  private readonly _document: Document,
90
90
  private readonly _config: ICharAtlasConfig,
91
- private readonly _unicodeService: IUnicodeService
91
+ private readonly _unicodeService: IUnicodeService,
92
+ private readonly _logService: ILogService
92
93
  ) {
93
94
  this._createNewPage();
94
95
  this._tmpCanvas = createCanvas(
@@ -119,7 +120,7 @@ export class TextureAtlas implements ITextureAtlas {
119
120
 
120
121
  private _doWarmUp(): void {
121
122
  // Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
122
- const queue = new IdleTaskQueue();
123
+ const queue = new IdleTaskQueue(this._logService);
123
124
  for (let i = 33; i < 126; i++) {
124
125
  queue.enqueue(() => {
125
126
  if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
@@ -132,7 +133,9 @@ export class TextureAtlas implements ITextureAtlas {
132
133
 
133
134
  private _requestClearModel = false;
134
135
  public beginFrame(): boolean {
135
- return this._requestClearModel;
136
+ const result = this._requestClearModel;
137
+ this._requestClearModel = false;
138
+ return result;
136
139
  }
137
140
 
138
141
  public clearTexture(): void {
@@ -176,12 +179,23 @@ export class TextureAtlas implements ITextureAtlas {
176
179
 
177
180
  // Gather details of the merge
178
181
  const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
182
+
183
+ // Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
184
+ // effectively reduce page count and merging would cause issues.
185
+ if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
186
+ const newPage = new AtlasPage(this._document, this._textureSize);
187
+ this._pages.push(newPage);
188
+ this._activePages.push(newPage);
189
+ this._onAddTextureAtlasCanvas.fire(newPage.canvas);
190
+ return newPage;
191
+ }
192
+
179
193
  const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
180
194
  const mergedPageIndex = this.pages.length - mergingPages.length;
181
195
 
182
196
  // Merge into the new page
183
197
  const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
184
- mergedPage.version++;
198
+ mergedPage.version = ++AtlasPage.nextVersion;
185
199
 
186
200
  // Delete the pages, shifting glyph texture pages as needed
187
201
  for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) {
@@ -239,7 +253,7 @@ export class TextureAtlas implements ITextureAtlas {
239
253
  for (const g of adjustingPage.glyphs) {
240
254
  g.texturePage--;
241
255
  }
242
- adjustingPage.version++;
256
+ adjustingPage.version = ++AtlasPage.nextVersion;
243
257
  }
244
258
  }
245
259
 
@@ -399,7 +413,7 @@ export class TextureAtlas implements ITextureAtlas {
399
413
  const cache = this._getContrastCache(dim);
400
414
  const adjustedColor = cache.getColor(bg, fg);
401
415
  if (adjustedColor !== undefined) {
402
- return adjustedColor || undefined;
416
+ return adjustedColor ?? undefined;
403
417
  }
404
418
 
405
419
  const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
@@ -514,7 +528,8 @@ export class TextureAtlas implements ITextureAtlas {
514
528
  // Draw custom characters if applicable
515
529
  let customGlyph = false;
516
530
  if (this._config.customGlyphs !== false) {
517
- customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css);
531
+ const variantOffset = this._workAttributeData.getUnderlineVariantOffset();
532
+ customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
518
533
  }
519
534
 
520
535
  // Whether to clear pixels based on a threshold difference between the glyph color and the
@@ -551,61 +566,67 @@ export class TextureAtlas implements ITextureAtlas {
551
566
  }
552
567
  this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css;
553
568
  }
569
+ this._tmpCtx.fillStyle = this._tmpCtx.strokeStyle;
554
570
 
555
571
  // Underline style/stroke
556
572
  this._tmpCtx.beginPath();
557
573
  const xLeft = padding;
558
- const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset - (restrictToCellHeight ? lineWidth * 2 : 0);
559
- const yMid = yTop + lineWidth;
560
- const yBot = yTop + lineWidth * 2;
574
+ const yTopDefault = Math.ceil(padding + this._config.deviceCharHeight) - yOffset - (restrictToCellHeight ? lineWidth * 2 : 0);
575
+ const yBotDefault = yTopDefault + lineWidth * 2;
561
576
  let nextOffset = this._workAttributeData.getUnderlineVariantOffset();
577
+ let yTop = 0;
578
+ let yBot = 0;
562
579
 
563
580
  for (let i = 0; i < chWidth; i++) {
581
+ let wasFilled = false;
564
582
  this._tmpCtx.save();
583
+ yTop = yTopDefault;
584
+ yBot = yBotDefault;
565
585
  const xChLeft = xLeft + i * this._config.deviceCellWidth;
566
586
  const xChRight = xLeft + (i + 1) * this._config.deviceCellWidth;
567
- const xChMid = xChLeft + this._config.deviceCellWidth / 2;
568
587
  switch (this._workAttributeData.extended.underlineStyle) {
569
588
  case UnderlineStyle.DOUBLE:
570
- this._tmpCtx.moveTo(xChLeft, yTop);
571
- this._tmpCtx.lineTo(xChRight, yTop);
572
- this._tmpCtx.moveTo(xChLeft, yBot);
573
- this._tmpCtx.lineTo(xChRight, yBot);
589
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
590
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
591
+ this._tmpCtx.moveTo(xChLeft, yBotDefault);
592
+ this._tmpCtx.lineTo(xChRight, yBotDefault);
574
593
  break;
575
594
  case UnderlineStyle.CURLY:
576
- // Choose the bezier top and bottom based on the device pixel ratio, the curly line is
577
- // made taller when the line width is as otherwise it's not very clear otherwise.
578
- const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.deviceCharHeight - lineWidth / 2) - yOffset;
579
- const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.deviceCharHeight + lineWidth / 2) - yOffset;
580
- // Clip the left and right edges of the underline such that it can be drawn just outside
581
- // the edge of the cell to ensure a continuous stroke when there are multiple underlined
582
- // glyphs adjacent to one another.
595
+ yTop = this._config.deviceCharHeight + 1;
596
+ yBot = yTop + 3 * this._config.devicePixelRatio;
597
+
583
598
  const clipRegion = new Path2D();
584
599
  clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, yBot - yTop);
585
600
  this._tmpCtx.clip(clipRegion);
586
- // Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other
587
- // cells
588
- this._tmpCtx.moveTo(xChLeft - this._config.deviceCellWidth / 2, yMid);
589
- this._tmpCtx.bezierCurveTo(
590
- xChLeft - this._config.deviceCellWidth / 2, yCurlyTop,
591
- xChLeft, yCurlyTop,
592
- xChLeft, yMid
593
- );
594
- this._tmpCtx.bezierCurveTo(
595
- xChLeft, yCurlyBot,
596
- xChMid, yCurlyBot,
597
- xChMid, yMid
598
- );
599
- this._tmpCtx.bezierCurveTo(
600
- xChMid, yCurlyTop,
601
- xChRight, yCurlyTop,
602
- xChRight, yMid
603
- );
604
- this._tmpCtx.bezierCurveTo(
605
- xChRight, yCurlyBot,
606
- xChRight + this._config.deviceCellWidth / 2, yCurlyBot,
607
- xChRight + this._config.deviceCellWidth / 2, yMid
608
- );
601
+
602
+ // Draw a zigzag pattern, this is derived from the SVG used in monaco for the same
603
+ // style. The viewbox is 6x3 so scale it using that.
604
+ const cellW = this._config.deviceCellWidth;
605
+ const curlyH = (yBot - yTop);
606
+ const scaleX = cellW / 6;
607
+ const scaleY = curlyH / 3;
608
+
609
+ const polygons: number[][] = [
610
+ [0, 2, 1, 3, 2.4, 3, 0, 0.6],
611
+ [5.5, 0, 2.5, 3, 1.1, 3, 4.1, 0],
612
+ [4, 0, 6, 2, 6, 0.6, 5.4, 0],
613
+ ];
614
+
615
+ for (const polygon of polygons) {
616
+ this._tmpCtx.beginPath();
617
+ for (let i = 0; i < polygon.length; i += 2) {
618
+ const x = xChLeft + polygon[i] * scaleX;
619
+ const y = yBot - polygon[i + 1] * scaleY;
620
+ if (i === 0) {
621
+ this._tmpCtx.moveTo(x, y);
622
+ } else {
623
+ this._tmpCtx.lineTo(x, y);
624
+ }
625
+ }
626
+ this._tmpCtx.closePath();
627
+ this._tmpCtx.fill();
628
+ }
629
+ wasFilled = true;
609
630
  break;
610
631
  case UnderlineStyle.DOTTED:
611
632
  const offsetWidth = nextOffset === 0 ? 0 :
@@ -614,14 +635,14 @@ export class TextureAtlas implements ITextureAtlas {
614
635
  const isLineStart = nextOffset >= lineWidth ? false : true;
615
636
  if (isLineStart === false || offsetWidth === 0) {
616
637
  this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]);
617
- this._tmpCtx.moveTo(xChLeft + offsetWidth, yTop);
618
- this._tmpCtx.lineTo(xChRight, yTop);
638
+ this._tmpCtx.moveTo(xChLeft + offsetWidth, yTopDefault);
639
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
619
640
  } else {
620
641
  this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]);
621
- this._tmpCtx.moveTo(xChLeft, yTop);
622
- this._tmpCtx.lineTo(xChLeft + offsetWidth, yTop);
623
- this._tmpCtx.moveTo(xChLeft + offsetWidth + lineWidth, yTop);
624
- this._tmpCtx.lineTo(xChRight, yTop);
642
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
643
+ this._tmpCtx.lineTo(xChLeft + offsetWidth, yTopDefault);
644
+ this._tmpCtx.moveTo(xChLeft + offsetWidth + lineWidth, yTopDefault);
645
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
625
646
  }
626
647
  nextOffset = computeNextVariantOffset(xChRight - xChLeft, lineWidth, nextOffset);
627
648
  break;
@@ -634,16 +655,18 @@ export class TextureAtlas implements ITextureAtlas {
634
655
  const gap = Math.floor(gapRatio * xChWidth);
635
656
  const end = xChWidth - line - gap;
636
657
  this._tmpCtx.setLineDash([line, gap, end]);
637
- this._tmpCtx.moveTo(xChLeft, yTop);
638
- this._tmpCtx.lineTo(xChRight, yTop);
658
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
659
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
639
660
  break;
640
661
  case UnderlineStyle.SINGLE:
641
662
  default:
642
- this._tmpCtx.moveTo(xChLeft, yTop);
643
- this._tmpCtx.lineTo(xChRight, yTop);
663
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
664
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
644
665
  break;
645
666
  }
646
- this._tmpCtx.stroke();
667
+ if (!wasFilled) {
668
+ this._tmpCtx.stroke();
669
+ }
647
670
  this._tmpCtx.restore();
648
671
  }
649
672
  this._tmpCtx.restore();
@@ -916,7 +939,7 @@ export class TextureAtlas implements ITextureAtlas {
916
939
  rasterizedGlyph.size.y
917
940
  );
918
941
  activePage.addGlyph(rasterizedGlyph);
919
- activePage.version++;
942
+ activePage.version = ++AtlasPage.nextVersion;
920
943
 
921
944
  return rasterizedGlyph;
922
945
  }
@@ -1026,9 +1049,13 @@ class AtlasPage {
1026
1049
  }
1027
1050
 
1028
1051
  /**
1029
- * Used to check whether the canvas of the atlas page has changed.
1052
+ * Monotonically increasing across all atlas pages globally. Used to detect when the texture
1053
+ * unit at a given index needs to be re-uploaded — both for content changes within the same
1054
+ * page and for a page object swap at the same index (which happens after a page merge,
1055
+ * where a per-page counter could coincide with the previously-bound page's value).
1030
1056
  */
1031
- public version = 0;
1057
+ public static nextVersion: number = 0;
1058
+ public version = ++AtlasPage.nextVersion;
1032
1059
 
1033
1060
  // Texture atlas current positioning data. The texture packing strategy used is to fill from
1034
1061
  // left-to-right and top-to-bottom. When the glyph being written is less than half of the current
@@ -1071,7 +1098,7 @@ class AtlasPage {
1071
1098
  this.currentRow.y = 0;
1072
1099
  this.currentRow.height = 0;
1073
1100
  this.fixedRows.length = 0;
1074
- this.version++;
1101
+ this.version = ++AtlasPage.nextVersion;
1075
1102
  }
1076
1103
  }
1077
1104
 
package/src/Types.ts CHANGED
@@ -7,7 +7,7 @@ import { FontWeight } from '@xterm/xterm';
7
7
  import { IColorSet } from 'browser/Types';
8
8
  import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
9
9
  import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types';
10
- import type { Event } from 'vs/base/common/event';
10
+ import type { IEvent } from 'common/Event';
11
11
 
12
12
  export interface IRenderModel {
13
13
  cells: Uint32Array;
@@ -58,8 +58,8 @@ export interface ICharAtlasConfig {
58
58
  export interface ITextureAtlas extends IDisposable {
59
59
  readonly pages: { canvas: HTMLCanvasElement, version: number }[];
60
60
 
61
- onAddTextureAtlasCanvas: Event<HTMLCanvasElement>;
62
- onRemoveTextureAtlasCanvas: Event<HTMLCanvasElement>;
61
+ onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
62
+ onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
63
63
 
64
64
  /**
65
65
  * Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
package/src/WebglAddon.ts CHANGED
@@ -7,15 +7,14 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm';
7
7
  import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
8
8
  import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
9
9
  import { ITerminal } from 'browser/Types';
10
- import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
10
+ import { Disposable, toDisposable } from 'common/Lifecycle';
11
11
  import { getSafariVersion, isSafari } from 'common/Platform';
12
- import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
12
+ import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
13
13
  import { IWebGL2RenderingContext } from './Types';
14
14
  import { WebglRenderer } from './WebglRenderer';
15
- import { setTraceLogger } from 'common/services/LogService';
16
- import { Emitter, Event } from 'vs/base/common/event';
15
+ import { Emitter, EventUtils } from 'common/Event';
17
16
 
18
- export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi {
17
+ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi {
19
18
  private _terminal?: Terminal;
20
19
  private _renderer?: WebglRenderer;
21
20
 
@@ -66,13 +65,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
66
65
  const charSizeService: ICharSizeService = unsafeCore._charSizeService;
67
66
  const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
68
67
  const decorationService: IDecorationService = unsafeCore._decorationService;
69
- const logService: ILogService = unsafeCore._logService;
70
68
  const themeService: IThemeService = unsafeCore._themeService;
71
69
 
72
- // Set trace logger just in case it hasn't been yet which could happen when the addon is
73
- // bundled separately to the core module
74
- setTraceLogger(logService);
75
-
76
70
  this._renderer = this._register(new WebglRenderer(
77
71
  terminal,
78
72
  characterJoinerService,
@@ -85,10 +79,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
85
79
  this._customGlyphs,
86
80
  this._preserveDrawingBuffer
87
81
  ));
88
- this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss));
89
- this._register(Event.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
90
- this._register(Event.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
91
- this._register(Event.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
82
+ this._register(EventUtils.forward(this._renderer.onContextLoss, this._onContextLoss));
83
+ this._register(EventUtils.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
84
+ this._register(EventUtils.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
85
+ this._register(EventUtils.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
92
86
  renderService.setRenderer(this._renderer);
93
87
 
94
88
  this._register(toDisposable(() => {
@@ -13,23 +13,29 @@ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeS
13
13
  import { CharData, IBufferLine, ICellData } from 'common/Types';
14
14
  import { AttributeData } from 'common/buffer/AttributeData';
15
15
  import { CellData } from 'common/buffer/CellData';
16
- import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
16
+ import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
17
+ import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
17
18
  import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
18
19
  import { Terminal } from '@xterm/xterm';
19
20
  import { GlyphRenderer } from './GlyphRenderer';
20
21
  import { RectangleRenderer } from './RectangleRenderer';
21
- 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';
22
+ import { COMBINED_CHAR_BIT_MASK, RenderModel, RenderModelConstants } from './RenderModel';
22
23
  import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
23
24
  import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
24
25
  import { IRenderLayer } from './renderLayer/Types';
25
- import { Emitter, Event } from 'vs/base/common/event';
26
- import { addDisposableListener } from 'vs/base/browser/dom';
27
- import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
26
+ import { Emitter, EventUtils } from 'common/Event';
27
+ import { addDisposableListener } from 'browser/Dom';
28
+ import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
28
29
  import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
29
30
 
31
+ const enum Constants {
32
+ MERGE_RETRY_LIMIT = 32
33
+ }
34
+
30
35
  export class WebglRenderer extends Disposable implements IRenderer {
31
36
  private _renderLayers: IRenderLayer[];
32
- private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
37
+ private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = this._register(new MutableDisposable());
38
+ private _textBlinkStateManager: TextBlinkStateManager;
33
39
  private _charAtlasDisposable = this._register(new MutableDisposable());
34
40
  private _charAtlas: ITextureAtlas | undefined;
35
41
  private _devicePixelRatio: number;
@@ -37,8 +43,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
37
43
  private _observerDisposable = this._register(new MutableDisposable());
38
44
 
39
45
  private _model: RenderModel = new RenderModel();
46
+ private _rowHasBlinkingCells: boolean[] = [];
47
+ private _rowHasBlinkingCellsCount: number = 0;
40
48
  private _workCell: ICellData = new CellData();
41
- private _workCell2: ICellData = new CellData();
42
49
  private _cellColorResolver: CellColorResolver;
43
50
 
44
51
  private _canvas: HTMLCanvasElement;
@@ -105,6 +112,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
105
112
  this._updateDimensions();
106
113
  this._updateCursorBlink();
107
114
  this._register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
115
+ this._textBlinkStateManager = this._register(new TextBlinkStateManager(
116
+ () => this._requestRedrawViewport(),
117
+ this._coreBrowserService,
118
+ this._optionsService
119
+ ));
120
+ this._resetBlinkingRowState();
108
121
 
109
122
  this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
110
123
 
@@ -136,6 +149,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
136
149
  this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
137
150
  }));
138
151
 
152
+ this._register(addDisposableListener(this._coreBrowserService.mainDocument, 'mousedown', () => this._cursorBlinkStateManager.value?.restartBlinkAnimation()));
153
+
139
154
  this._core.screenElement!.appendChild(this._canvas);
140
155
 
141
156
  [this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
@@ -143,6 +158,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
143
158
  this._isAttached = this._core.screenElement!.isConnected;
144
159
 
145
160
  this._register(toDisposable(() => {
161
+ clearTimeout(this._contextRestorationTimeout);
162
+ this._contextRestorationTimeout = undefined;
146
163
  for (const l of this._renderLayers) {
147
164
  l.dispose();
148
165
  }
@@ -176,6 +193,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
176
193
  this._updateDimensions();
177
194
 
178
195
  this._model.resize(this._terminal.cols, this._terminal.rows);
196
+ this._resetBlinkingRowState();
179
197
 
180
198
  // Resize all render layers
181
199
  for (const l of this._renderLayers) {
@@ -202,6 +220,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
202
220
  // Force a full refresh. Resizing `_glyphRenderer` should clear it already,
203
221
  // so there is no need to clear it again here.
204
222
  this._clearModel(false);
223
+
224
+ // Render synchronously to avoid flicker when the canvas is cleared
225
+ this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1, sync: true });
205
226
  }
206
227
 
207
228
  public handleCharSizeChanged(): void {
@@ -226,6 +247,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
226
247
  this._requestRedrawViewport();
227
248
  }
228
249
 
250
+ public handleViewportVisibilityChange(isVisible: boolean): void {
251
+ this._textBlinkStateManager.setViewportVisible(isVisible);
252
+ }
253
+
229
254
  public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
230
255
  for (const l of this._renderLayers) {
231
256
  l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
@@ -285,8 +310,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
285
310
  if (this._charAtlas !== atlas) {
286
311
  this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
287
312
  this._charAtlasDisposable.value = combinedDisposable(
288
- Event.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
289
- Event.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
313
+ EventUtils.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
314
+ EventUtils.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
290
315
  );
291
316
  }
292
317
  this._charAtlas = atlas;
@@ -318,6 +343,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
318
343
  l.reset(this._terminal);
319
344
  }
320
345
 
346
+ this._resetBlinkingRowState();
347
+ this._textBlinkStateManager.setNeedsBlinkInViewport(false);
348
+
321
349
  this._cursorBlinkStateManager.value?.restartBlinkAnimation();
322
350
  this._updateCursorBlink();
323
351
  }
@@ -353,6 +381,19 @@ export class WebglRenderer extends Disposable implements IRenderer {
353
381
  this._updateModel(start, end);
354
382
  }
355
383
 
384
+ // A mid-update atlas page merge invalidates vertex data and may not bump the host
385
+ // page's version, so re-run the update and force a full texture rebind.
386
+ let merged = false;
387
+ let mergeRetries = 0;
388
+ while (this._charAtlas && this._glyphRenderer.value.beginFrame() && mergeRetries++ < Constants.MERGE_RETRY_LIMIT) {
389
+ merged = true;
390
+ this._clearModel(true);
391
+ this._updateModel(0, this._terminal.rows - 1);
392
+ }
393
+ if (merged) {
394
+ this._glyphRenderer.value.invalidateAtlasTextures();
395
+ }
396
+
356
397
  // Render
357
398
  this._rectangleRenderer.value.renderBackgrounds();
358
399
  this._glyphRenderer.value.render(this._model);
@@ -415,6 +456,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
415
456
  for (y = start; y <= end; y++) {
416
457
  row = y + terminal.buffer.ydisp;
417
458
  line = terminal.buffer.lines.get(row)!;
459
+ let rowHasBlinkingCells = false;
418
460
  this._model.lineLengths[y] = 0;
419
461
  isCursorRow = cursorY === row;
420
462
  skipJoinedCheckUntilX = 0;
@@ -470,10 +512,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
470
512
 
471
513
  chars = cell.getChars();
472
514
  code = cell.getCode();
473
- i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
515
+ i = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
516
+
517
+ if (!rowHasBlinkingCells && cell.isBlink()) {
518
+ rowHasBlinkingCells = true;
519
+ }
474
520
 
475
521
  // Load colors/resolve overrides into work colors
476
- this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width);
522
+ this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width, this.dimensions.device.cell.height);
477
523
 
478
524
  // Override colors for cursor cell
479
525
  if (isCursorVisible && row === cursorY) {
@@ -501,15 +547,19 @@ export class WebglRenderer extends Disposable implements IRenderer {
501
547
  }
502
548
  }
503
549
 
550
+ if (this._textBlinkStateManager.isEnabled && !this._textBlinkStateManager.isBlinkOn && cell.isBlink()) {
551
+ this._cellColorResolver.result.fg |= FgFlags.INVISIBLE;
552
+ }
553
+
504
554
  if (code !== NULL_CELL_CODE) {
505
555
  this._model.lineLengths[y] = x + 1;
506
556
  }
507
557
 
508
558
  // Nothing has changed, no updates needed
509
559
  if (this._model.cells[i] === code &&
510
- this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
511
- this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
512
- this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
560
+ this._model.cells[i + RenderModelConstants.BG_OFFSET] === this._cellColorResolver.result.bg &&
561
+ this._model.cells[i + RenderModelConstants.FG_OFFSET] === this._cellColorResolver.result.fg &&
562
+ this._model.cells[i + RenderModelConstants.EXT_OFFSET] === this._cellColorResolver.result.ext) {
513
563
  continue;
514
564
  }
515
565
 
@@ -522,9 +572,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
522
572
 
523
573
  // Cache the results in the model
524
574
  this._model.cells[i] = code;
525
- this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
526
- this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
527
- this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
575
+ this._model.cells[i + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
576
+ this._model.cells[i + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
577
+ this._model.cells[i + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
528
578
 
529
579
  width = cell.getWidth();
530
580
  this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, width, lastBg);
@@ -535,23 +585,43 @@ export class WebglRenderer extends Disposable implements IRenderer {
535
585
 
536
586
  // Null out non-first cells
537
587
  for (x++; x <= lastCharX; x++) {
538
- j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
588
+ j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
539
589
  this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
540
590
  this._model.cells[j] = NULL_CELL_CODE;
541
591
  // Don't re-resolve the cell color since multi-colored ligature backgrounds are not
542
592
  // supported
543
- this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
544
- this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
545
- this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
593
+ this._model.cells[j + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
594
+ this._model.cells[j + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
595
+ this._model.cells[j + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
546
596
  }
547
597
  x--; // Go back to the previous update cell for next iteration
548
598
  }
549
599
  }
600
+ this._setRowBlinkState(y, rowHasBlinkingCells);
550
601
  }
551
602
  if (modelUpdated) {
552
603
  this._rectangleRenderer.value!.updateBackgrounds(this._model);
553
604
  }
554
605
  this._rectangleRenderer.value!.updateCursor(this._model);
606
+ this._updateTextBlinkState();
607
+ }
608
+
609
+ private _resetBlinkingRowState(): void {
610
+ this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
611
+ this._rowHasBlinkingCellsCount = 0;
612
+ }
613
+
614
+ private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void {
615
+ const previous = this._rowHasBlinkingCells[row];
616
+ if (previous === hasBlinkingCells) {
617
+ return;
618
+ }
619
+ this._rowHasBlinkingCells[row] = hasBlinkingCells;
620
+ this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1;
621
+ }
622
+
623
+ private _updateTextBlinkState(): void {
624
+ this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0);
555
625
  }
556
626
 
557
627
  /**
@@ -617,7 +687,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
617
687
  // the change as it's an exact multiple of the cell sizes.
618
688
  this._canvas.width = width;
619
689
  this._canvas.height = height;
620
- this._requestRedrawViewport();
690
+ // Render synchronously to avoid flicker when the canvas is cleared
691
+ this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1, sync: true });
621
692
  }
622
693
 
623
694
  private _requestRedrawViewport(): void {