@xterm/addon-webgl 0.20.0-beta.9 → 0.20.0-beta.90

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xterm/addon-webgl",
3
- "version": "0.20.0-beta.9",
3
+ "version": "0.20.0-beta.90",
4
4
  "author": {
5
5
  "name": "The xterm.js authors",
6
6
  "url": "https://xtermjs.org/"
@@ -23,8 +23,8 @@
23
23
  "prepublishOnly": "npm run package",
24
24
  "start": "node ../../demo/start"
25
25
  },
26
- "commit": "579573e77d2bcf50c056543fad6f1334b957e6d1",
26
+ "commit": "94a264679e6bd51b7021ca59e3b2e1e80d02c633",
27
27
  "peerDependencies": {
28
- "@xterm/xterm": "^6.1.0-beta.10"
28
+ "@xterm/xterm": "^6.1.0-beta.91"
29
29
  }
30
30
  }
@@ -32,9 +32,10 @@ export function acquireTextureAtlas(
32
32
  deviceCharWidth: number,
33
33
  deviceCharHeight: number,
34
34
  devicePixelRatio: number,
35
- deviceMaxTextureSize: number
35
+ deviceMaxTextureSize: number,
36
+ customGlyphs: boolean = true
36
37
  ): ITextureAtlas {
37
- const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize);
38
+ const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize, customGlyphs);
38
39
 
39
40
  // Check to see if the terminal already owns this config
40
41
  for (let i = 0; i < charAtlasCache.length; i++) {
@@ -9,7 +9,7 @@ import { ITerminalOptions } from '@xterm/xterm';
9
9
  import { IColorSet, ReadonlyColorSet } from 'browser/Types';
10
10
  import { NULL_COLOR } from 'common/Color';
11
11
 
12
- export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig {
12
+ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number, customGlyphs: boolean = true): ICharAtlasConfig {
13
13
  // null out some fields that don't matter
14
14
  const clonedColors: IColorSet = {
15
15
  foreground: colors.foreground,
@@ -32,7 +32,7 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
32
32
  halfContrastCache: colors.halfContrastCache
33
33
  };
34
34
  return {
35
- customGlyphs: options.customGlyphs,
35
+ customGlyphs,
36
36
  devicePixelRatio,
37
37
  deviceMaxTextureSize,
38
38
  letterSpacing: options.letterSpacing,
@@ -3,12 +3,15 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
+ import { RendererConstants } from 'browser/renderer/shared/Constants';
6
7
  import { ICoreBrowserService } from 'browser/services/Services';
7
8
 
8
- /**
9
- * The time between cursor blinks.
10
- */
11
- const BLINK_INTERVAL = 600;
9
+ const enum Constants {
10
+ /**
11
+ * The time between cursor blinks.
12
+ */
13
+ BLINK_INTERVAL = 600,
14
+ }
12
15
 
13
16
  export class CursorBlinkStateManager {
14
17
  public isCursorVisible: boolean;
@@ -16,6 +19,8 @@ export class CursorBlinkStateManager {
16
19
  private _animationFrame: number | undefined;
17
20
  private _blinkStartTimeout: number | undefined;
18
21
  private _blinkInterval: number | undefined;
22
+ private _idleTimeout: number | undefined;
23
+ private _isIdlePaused: boolean = false;
19
24
 
20
25
  /**
21
26
  * The time at which the animation frame was restarted, this is used on the
@@ -31,6 +36,7 @@ export class CursorBlinkStateManager {
31
36
  this.isCursorVisible = true;
32
37
  if (this._coreBrowserService.isFocused) {
33
38
  this._restartInterval();
39
+ this._resetIdleTimer();
34
40
  }
35
41
  }
36
42
 
@@ -49,9 +55,16 @@ export class CursorBlinkStateManager {
49
55
  this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
50
56
  this._animationFrame = undefined;
51
57
  }
58
+ if (this._idleTimeout) {
59
+ this._coreBrowserService.window.clearTimeout(this._idleTimeout);
60
+ this._idleTimeout = undefined;
61
+ }
52
62
  }
53
63
 
54
64
  public restartBlinkAnimation(): void {
65
+ if (this._isIdlePaused) {
66
+ this._resetIdleTimer();
67
+ }
55
68
  if (this.isPaused) {
56
69
  return;
57
70
  }
@@ -67,7 +80,7 @@ export class CursorBlinkStateManager {
67
80
  }
68
81
  }
69
82
 
70
- private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
83
+ private _restartInterval(timeToStart: number = Constants.BLINK_INTERVAL): void {
71
84
  // Clear any existing interval
72
85
  if (this._blinkInterval) {
73
86
  this._coreBrowserService.window.clearInterval(this._blinkInterval);
@@ -82,7 +95,7 @@ export class CursorBlinkStateManager {
82
95
  // Check if another animation restart was requested while this was being
83
96
  // started
84
97
  if (this._animationTimeRestarted) {
85
- const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
98
+ const time = Constants.BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
86
99
  this._animationTimeRestarted = undefined;
87
100
  if (time > 0) {
88
101
  this._restartInterval(time);
@@ -103,7 +116,7 @@ export class CursorBlinkStateManager {
103
116
  if (this._animationTimeRestarted) {
104
117
  // calc time diff
105
118
  // Make restart interval do a setTimeout initially?
106
- const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
119
+ const time = Constants.BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
107
120
  this._animationTimeRestarted = undefined;
108
121
  this._restartInterval(time);
109
122
  return;
@@ -115,12 +128,13 @@ export class CursorBlinkStateManager {
115
128
  this._renderCallback();
116
129
  this._animationFrame = undefined;
117
130
  });
118
- }, BLINK_INTERVAL);
131
+ }, Constants.BLINK_INTERVAL);
119
132
  }, timeToStart);
120
133
  }
121
134
 
122
135
  public pause(): void {
123
136
  this.isCursorVisible = true;
137
+ this._isIdlePaused = false;
124
138
  if (this._blinkInterval) {
125
139
  this._coreBrowserService.window.clearInterval(this._blinkInterval);
126
140
  this._blinkInterval = undefined;
@@ -133,6 +147,10 @@ export class CursorBlinkStateManager {
133
147
  this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
134
148
  this._animationFrame = undefined;
135
149
  }
150
+ if (this._idleTimeout) {
151
+ this._coreBrowserService.window.clearTimeout(this._idleTimeout);
152
+ this._idleTimeout = undefined;
153
+ }
136
154
  }
137
155
 
138
156
  public resume(): void {
@@ -141,6 +159,47 @@ export class CursorBlinkStateManager {
141
159
 
142
160
  this._animationTimeRestarted = undefined;
143
161
  this._restartInterval();
162
+ this._resetIdleTimer();
144
163
  this.restartBlinkAnimation();
145
164
  }
165
+
166
+ /**
167
+ * Resets the idle timer. If the terminal is idle for the idle timeout period,
168
+ * the cursor blinking will stop.
169
+ */
170
+ private _resetIdleTimer(): void {
171
+ this._isIdlePaused = false;
172
+ if (this._idleTimeout) {
173
+ this._coreBrowserService.window.clearTimeout(this._idleTimeout);
174
+ }
175
+ this._idleTimeout = this._coreBrowserService.window.setTimeout(() => {
176
+ this._stopBlinkingDueToIdle();
177
+ }, RendererConstants.CURSOR_BLINK_IDLE_TIMEOUT);
178
+ }
179
+
180
+ /**
181
+ * Stops cursor blinking due to idle timeout.
182
+ */
183
+ private _stopBlinkingDueToIdle(): void {
184
+ // Make cursor visible and stop blinking
185
+ this.isCursorVisible = true;
186
+ this._isIdlePaused = true;
187
+ if (this._blinkInterval) {
188
+ this._coreBrowserService.window.clearInterval(this._blinkInterval);
189
+ this._blinkInterval = undefined;
190
+ }
191
+ if (this._blinkStartTimeout) {
192
+ this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
193
+ this._blinkStartTimeout = undefined;
194
+ }
195
+ if (this._animationFrame) {
196
+ this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
197
+ this._animationFrame = undefined;
198
+ }
199
+ // Clear the idle timeout as we've already acted on it
200
+ this._coreBrowserService.window.clearTimeout(this._idleTimeout);
201
+ this._idleTimeout = undefined;
202
+ // Trigger a render to show the cursor in its final visible state
203
+ this._renderCallback();
204
+ }
146
205
  }
@@ -514,7 +514,7 @@ export class TextureAtlas implements ITextureAtlas {
514
514
  // Draw custom characters if applicable
515
515
  let customGlyph = false;
516
516
  if (this._config.customGlyphs !== false) {
517
- customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css);
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);
518
518
  }
519
519
 
520
520
  // Whether to clear pixels based on a threshold difference between the glyph color and the
@@ -551,61 +551,67 @@ export class TextureAtlas implements ITextureAtlas {
551
551
  }
552
552
  this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css;
553
553
  }
554
+ this._tmpCtx.fillStyle = this._tmpCtx.strokeStyle;
554
555
 
555
556
  // Underline style/stroke
556
557
  this._tmpCtx.beginPath();
557
558
  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;
559
+ const yTopDefault = Math.ceil(padding + this._config.deviceCharHeight) - yOffset - (restrictToCellHeight ? lineWidth * 2 : 0);
560
+ const yBotDefault = yTopDefault + lineWidth * 2;
561
561
  let nextOffset = this._workAttributeData.getUnderlineVariantOffset();
562
+ let yTop = 0;
563
+ let yBot = 0;
562
564
 
563
565
  for (let i = 0; i < chWidth; i++) {
566
+ let wasFilled = false;
564
567
  this._tmpCtx.save();
568
+ yTop = yTopDefault;
569
+ yBot = yBotDefault;
565
570
  const xChLeft = xLeft + i * this._config.deviceCellWidth;
566
571
  const xChRight = xLeft + (i + 1) * this._config.deviceCellWidth;
567
- const xChMid = xChLeft + this._config.deviceCellWidth / 2;
568
572
  switch (this._workAttributeData.extended.underlineStyle) {
569
573
  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);
574
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
575
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
576
+ this._tmpCtx.moveTo(xChLeft, yBotDefault);
577
+ this._tmpCtx.lineTo(xChRight, yBotDefault);
574
578
  break;
575
579
  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.
580
+ yTop = this._config.deviceCharHeight + 1;
581
+ yBot = yTop + 3 * this._config.devicePixelRatio;
582
+
583
583
  const clipRegion = new Path2D();
584
584
  clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, yBot - yTop);
585
585
  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
- );
586
+
587
+ // Draw a zigzag pattern, this is derived from the SVG used in monaco for the same
588
+ // style. The viewbox is 6x3 so scale it using that.
589
+ const cellW = this._config.deviceCellWidth;
590
+ const curlyH = (yBot - yTop);
591
+ const scaleX = cellW / 6;
592
+ const scaleY = curlyH / 3;
593
+
594
+ const polygons: number[][] = [
595
+ [0, 2, 1, 3, 2.4, 3, 0, 0.6],
596
+ [5.5, 0, 2.5, 3, 1.1, 3, 4.1, 0],
597
+ [4, 0, 6, 2, 6, 0.6, 5.4, 0],
598
+ ];
599
+
600
+ for (const polygon of polygons) {
601
+ this._tmpCtx.beginPath();
602
+ for (let i = 0; i < polygon.length; i += 2) {
603
+ const x = xChLeft + polygon[i] * scaleX;
604
+ const y = yBot - polygon[i + 1] * scaleY;
605
+ if (i === 0) {
606
+ this._tmpCtx.moveTo(x, y);
607
+ } else {
608
+ this._tmpCtx.lineTo(x, y);
609
+ }
610
+ }
611
+ this._tmpCtx.closePath();
612
+ this._tmpCtx.fill();
613
+ }
614
+ wasFilled = true;
609
615
  break;
610
616
  case UnderlineStyle.DOTTED:
611
617
  const offsetWidth = nextOffset === 0 ? 0 :
@@ -614,14 +620,14 @@ export class TextureAtlas implements ITextureAtlas {
614
620
  const isLineStart = nextOffset >= lineWidth ? false : true;
615
621
  if (isLineStart === false || offsetWidth === 0) {
616
622
  this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]);
617
- this._tmpCtx.moveTo(xChLeft + offsetWidth, yTop);
618
- this._tmpCtx.lineTo(xChRight, yTop);
623
+ this._tmpCtx.moveTo(xChLeft + offsetWidth, yTopDefault);
624
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
619
625
  } else {
620
626
  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);
627
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
628
+ this._tmpCtx.lineTo(xChLeft + offsetWidth, yTopDefault);
629
+ this._tmpCtx.moveTo(xChLeft + offsetWidth + lineWidth, yTopDefault);
630
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
625
631
  }
626
632
  nextOffset = computeNextVariantOffset(xChRight - xChLeft, lineWidth, nextOffset);
627
633
  break;
@@ -634,16 +640,18 @@ export class TextureAtlas implements ITextureAtlas {
634
640
  const gap = Math.floor(gapRatio * xChWidth);
635
641
  const end = xChWidth - line - gap;
636
642
  this._tmpCtx.setLineDash([line, gap, end]);
637
- this._tmpCtx.moveTo(xChLeft, yTop);
638
- this._tmpCtx.lineTo(xChRight, yTop);
643
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
644
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
639
645
  break;
640
646
  case UnderlineStyle.SINGLE:
641
647
  default:
642
- this._tmpCtx.moveTo(xChLeft, yTop);
643
- this._tmpCtx.lineTo(xChRight, yTop);
648
+ this._tmpCtx.moveTo(xChLeft, yTopDefault);
649
+ this._tmpCtx.lineTo(xChRight, yTopDefault);
644
650
  break;
645
651
  }
646
- this._tmpCtx.stroke();
652
+ if (!wasFilled) {
653
+ this._tmpCtx.stroke();
654
+ }
647
655
  this._tmpCtx.restore();
648
656
  }
649
657
  this._tmpCtx.restore();
package/src/WebglAddon.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { ITerminalAddon, Terminal } from '@xterm/xterm';
7
- import type { WebglAddon as IWebglApi } from '@xterm/addon-webgl';
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
10
  import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -28,9 +28,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
28
28
  private readonly _onContextLoss = this._register(new Emitter<void>());
29
29
  public readonly onContextLoss = this._onContextLoss.event;
30
30
 
31
- constructor(
32
- private _preserveDrawingBuffer?: boolean
33
- ) {
31
+ private readonly _customGlyphs: boolean;
32
+ private readonly _preserveDrawingBuffer?: boolean;
33
+
34
+ constructor(options?: IWebglAddonOptions) {
34
35
  if (isSafari && getSafariVersion() < 16) {
35
36
  // Perform an extra check to determine if Webgl2 is manually enabled in developer settings
36
37
  const contextAttributes = {
@@ -44,6 +45,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
44
45
  }
45
46
  }
46
47
  super();
48
+ this._customGlyphs = options?.customGlyphs ?? true;
49
+ this._preserveDrawingBuffer = options?.preserveDrawingBuffer;
47
50
  }
48
51
 
49
52
  public activate(terminal: Terminal): void {
@@ -79,6 +82,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
79
82
  decorationService,
80
83
  optionsService,
81
84
  themeService,
85
+ this._customGlyphs,
82
86
  this._preserveDrawingBuffer
83
87
  ));
84
88
  this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss));
@@ -72,6 +72,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
72
72
  private readonly _decorationService: IDecorationService,
73
73
  private readonly _optionsService: IOptionsService,
74
74
  private readonly _themeService: IThemeService,
75
+ private readonly _customGlyphs: boolean = true,
75
76
  preserveDrawingBuffer?: boolean
76
77
  ) {
77
78
  super();
@@ -135,6 +136,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
135
136
  this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
136
137
  }));
137
138
 
139
+ this._register(addDisposableListener(this._coreBrowserService.mainDocument, 'mousedown', () => this._cursorBlinkStateManager.value?.restartBlinkAnimation()));
140
+
138
141
  this._core.screenElement!.appendChild(this._canvas);
139
142
 
140
143
  [this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
@@ -201,6 +204,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
201
204
  // Force a full refresh. Resizing `_glyphRenderer` should clear it already,
202
205
  // so there is no need to clear it again here.
203
206
  this._clearModel(false);
207
+
208
+ // Render synchronously to avoid flicker when the canvas is cleared
209
+ this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1, sync: true });
204
210
  }
205
211
 
206
212
  public handleCharSizeChanged(): void {
@@ -278,7 +284,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
278
284
  this.dimensions.device.char.width,
279
285
  this.dimensions.device.char.height,
280
286
  this._coreBrowserService.dpr,
281
- this._deviceMaxTextureSize
287
+ this._deviceMaxTextureSize,
288
+ this._customGlyphs
282
289
  );
283
290
  if (this._charAtlas !== atlas) {
284
291
  this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
@@ -615,7 +622,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
615
622
  // the change as it's an exact multiple of the cell sizes.
616
623
  this._canvas.width = width;
617
624
  this._canvas.height = height;
618
- this._requestRedrawViewport();
625
+ // Update the WebGL viewport to match the new canvas dimensions
626
+ this._gl.viewport(0, 0, width, height);
627
+ // Render synchronously to avoid flicker when the canvas is cleared
628
+ this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1, sync: true });
619
629
  }
620
630
 
621
631
  private _requestRedrawViewport(): void {