From cb10aadb0623c998d7d0a36e7b4026cd1080be34 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 19:18:26 -0700 Subject: [PATCH 01/18] Fix device pixel rounding errors in webgl renderer This uses the ResizeObserver devicePixelContentBoxSize API in order to fetch the exact device pixel dimensions from the browser. The old possibly blurry behavior is used as a fallback if that API is not available. Part of #2662 Part of microsoft/vscode#85154 --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 95 +++++++++++-------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 1e385b33..2c885ef6 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -11,7 +11,7 @@ import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; @@ -96,6 +96,31 @@ export class WebglRenderer extends Disposable implements IRenderer { this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); })); + // Observe any resizes to the canvas and extract the actual pixel size of the canvas if the + // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when + // converting between CSS pixels and device pixels which causes blurry rendering when device + // pixel ratio is not a round number. + let observer: ResizeObserver | undefined = new ResizeObserver((entries) => { + const entry = entries.find((entry) => entry.target === this._canvas); + if (!entry) { + return; + } + + // Disconnect if devicePixelContentBoxSize isn't supported by the browser + if (!('devicePixelContentBoxSize' in entry)) { + observer?.disconnect(); + observer = undefined; + return; + } + + this._setCanvasDevicePixelDimensions( + entry.devicePixelContentBoxSize[0].inlineSize, + entry.devicePixelContentBoxSize[0].blockSize + ); + }); + observer.observe(this._canvas, { box: ['device-pixel-content-box'] } as any); + this.register(toDisposable(() => observer?.disconnect())); + this._core.screenElement!.appendChild(this._canvas); this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions)); @@ -516,67 +541,61 @@ export class WebglRenderer extends Disposable implements IRenderer { return; } - // Calculate the scaled character width. Width is floored as it must be - // drawn to an integer grid in order for the CharAtlas "stamps" to not be - // blurry. When text is drawn to the grid not using the CharAtlas, it is - // clipped to ensure there is no overlap with the next cell. - - // NOTE: ceil fixes sometime, floor does others :s - + // Calculate the scaled character width. Width is floored as it must be drawn to an integer grid + // in order for the char atlas glyphs to not be blurry. this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio); - // Calculate the scaled character height. Height is ceiled in case - // devicePixelRatio is a floating point number in order to ensure there is - // enough space to draw the character to the cell. + // Calculate the scaled character height. Height is ceiled in case devicePixelRatio is a + // floating point number in order to ensure there is enough space to draw the character to the + // cell. this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio); - // Calculate the scaled cell height, if lineHeight is not 1 then the value - // will be floored because since lineHeight can never be lower then 1, there - // is a guarentee that the scaled line height will always be larger than - // scaled char height. + // Calculate the scaled cell height, if lineHeight is _not_ 1, the resulting value will be + // floored since lineHeight can never be lower then 1, this guarentees the scaled cell height + // will always be larger than scaled char height. this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!); - // Calculate the y coordinate within a cell that text should draw from in - // order to draw in the center of a cell. + // Calculate the y offset within a cell that glyph should draw at in order for it to be centered + // correctly within the cell. this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); // Calculate the scaled cell width, taking the letterSpacing into account. this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!); - // Calculate the x coordinate with a cell that text should draw from in - // order to draw in the center of a cell. + // Calculate the x offset with a cell that text should draw from in order for it to be centered + // correctly within the cell. this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2); - // Recalculate the canvas dimensions; scaled* define the actual number of - // pixel in the canvas + // Recalculate the canvas dimensions, the scaled dimensions define the actual number of pixel in + // the canvas this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; - // The the size of the canvas on the page. It's very important that this - // rounds to nearest integer and not ceils as browsers often set - // window.devicePixelRatio as something like 1.100000023841858, when it's - // actually 1.1. Ceiling causes blurriness as the backing canvas image is 1 - // pixel too large for the canvas element size. + // The the size of the canvas on the page. It's important that this rounds to nearest integer + // and not ceils as browsers often have floating point precision issues where + // `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when + // it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too + // large for the canvas element size. this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio); - // this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio; - // this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio; - - // Get the _actual_ dimensions of an individual cell. This needs to be - // derived from the canvasWidth/Height calculated above which takes into - // account window.devicePixelRatio. CharMeasure.width/height by itself is - // insufficient when the page is not at 100% zoom level as CharMeasure is - // measured in CSS pixels, but the actual char size on the canvas can + // Get the _actual_ dimensions of an individual cell. This needs to be derived from the + // canvas CSS dimensions calculated above which takes into account device pixel ratio. + // `CharMeasure.width`/`height` by itself is insufficient when device pixel ratio is not a round + // number as CharMeasure is measured in CSS pixels, since the actual device pixel char size will // differ. - // this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; - // this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; - - // This fixes 110% and 125%, not 150% or 175% though this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; } + private _setCanvasDevicePixelDimensions(width: number, height: number): void { + this.dimensions.scaledCanvasHeight = width; + this.dimensions.scaledCanvasWidth = height; + this._canvas.width = width; + this._canvas.height = height; + this._requestRedrawViewport(); + } + private _requestRedrawViewport(): void { this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); } From e25471361f127f534551f6ec78ebb16a71e6ca32 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 03:41:25 -0700 Subject: [PATCH 02/18] Fix device pixel value in canvas renderer --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 10 +-- src/browser/renderer/BaseRenderLayer.ts | 2 + src/browser/renderer/Renderer.ts | 80 ++++++++++--------- src/browser/renderer/Types.d.ts | 2 + 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 2c885ef6..b081fb1f 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -579,13 +579,9 @@ export class WebglRenderer extends Disposable implements IRenderer { this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio); - // Get the _actual_ dimensions of an individual cell. This needs to be derived from the - // canvas CSS dimensions calculated above which takes into account device pixel ratio. - // `CharMeasure.width`/`height` by itself is insufficient when device pixel ratio is not a round - // number as CharMeasure is measured in CSS pixels, since the actual device pixel char size will - // differ. - this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; - this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; + // Get the CSS dimensions of an individual cell. + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._devicePixelRatio; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._devicePixelRatio; } private _setCanvasDevicePixelDimensions(width: number, height: number): void { diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 0a9b8057..af3aabb4 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -48,6 +48,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { italic: false }; + public get canvas(): HTMLCanvasElement { return this._canvas; } + constructor( private _container: HTMLElement, id: string, diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index 8bc32278..04fdec77 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -8,7 +8,7 @@ import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer'; import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer'; import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services'; @@ -62,6 +62,33 @@ export class Renderer extends Disposable implements IRenderer { }; this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); + + // Observe any resizes to the canvas and extract the actual pixel size of the canvas if the + // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when + // converting between CSS pixels and device pixels which causes blurry rendering when device + // pixel ratio is not a round number. + const observedCanvas = this._renderLayers[0].canvas; + let observer: ResizeObserver | undefined = new ResizeObserver((entries) => { + const entry = entries.find((entry) => entry.target === observedCanvas); + if (!entry) { + return; + } + + // Disconnect if devicePixelContentBoxSize isn't supported by the browser + if (!('devicePixelContentBoxSize' in entry)) { + observer?.disconnect(); + observer = undefined; + return; + } + + this._setCanvasDevicePixelDimensions( + entry.devicePixelContentBoxSize[0].inlineSize, + entry.devicePixelContentBoxSize[0].blockSize + ); + }); + observer.observe(observedCanvas, { box: ['device-pixel-content-box'] } as any); + this.register(toDisposable(() => observer?.disconnect())); + this.onOptionsChanged(); } @@ -167,53 +194,32 @@ export class Renderer extends Disposable implements IRenderer { return; } - // Calculate the scaled character width. Width is floored as it must be - // drawn to an integer grid in order for the CharAtlas "stamps" to not be - // blurry. When text is drawn to the grid not using the CharAtlas, it is - // clipped to ensure there is no overlap with the next cell. + // See the WebGL renderer for an explanation of this section. this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio); - - // Calculate the scaled character height. Height is ceiled in case - // devicePixelRatio is a floating point number in order to ensure there is - // enough space to draw the character to the cell. this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); - - // Calculate the scaled cell height, if lineHeight is not 1 then the value - // will be floored because since lineHeight can never be lower then 1, there - // is a guarentee that the scaled line height will always be larger than - // scaled char height. this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight); - - // Calculate the y coordinate within a cell that text should draw from in - // order to draw in the center of a cell. this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); - - // Calculate the scaled cell width, taking the letterSpacing into account. this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing); - - // Calculate the x coordinate with a cell that text should draw from in - // order to draw in the center of a cell. this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2); - - // Recalculate the canvas dimensions; scaled* define the actual number of - // pixel in the canvas this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight; this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth; - - // The the size of the canvas on the page. It's very important that this - // rounds to nearest integer and not ceils as browsers often set - // window.devicePixelRatio as something like 1.100000023841858, when it's - // actually 1.1. Ceiling causes blurriness as the backing canvas image is 1 - // pixel too large for the canvas element size. this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); - - // Get the _actual_ dimensions of an individual cell. This needs to be - // derived from the canvasWidth/Height calculated above which takes into - // account window.devicePixelRatio. ICharSizeService.width/height by itself - // is insufficient when the page is not at 100% zoom level as it's measured - // in CSS pixels, but the actual char size on the canvas can differ. this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; } + + private _setCanvasDevicePixelDimensions(width: number, height: number): void { + this.dimensions.scaledCanvasHeight = width; + this.dimensions.scaledCanvasWidth = height; + // Resize all render layers + for (const l of this._renderLayers) { + l.resize(this.dimensions); + } + this._requestRedrawViewport(); + } + + private _requestRedrawViewport(): void { + this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); + } } diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index 6818a926..7410d481 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -56,6 +56,8 @@ export interface IRenderer extends IDisposable { } export interface IRenderLayer extends IDisposable { + readonly canvas: HTMLCanvasElement; + /** * Called when the terminal loses focus. */ From 3d6c25bf2de0a9ee625e61350ccb432c1a98f269 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 03:43:28 -0700 Subject: [PATCH 03/18] typescript@4.7 Required for devicePixelContentBoxSize ResizeObserver API --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cb0c1919..9db4d16f 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", "ts-loader": "^9.1.2", - "typescript": "^4.4.4", + "typescript": "4.7", "utf8": "^3.0.0", "webpack": "^5.61.0", "webpack-cli": "^4.9.1", diff --git a/yarn.lock b/yarn.lock index 7cb970bb..2a5c02bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3817,16 +3817,16 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@4.7: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + typescript@^4.2.3: version "4.6.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== -typescript@^4.4.4: - version "4.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" - integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== - unbox-primitive@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" From ad8404417edb072db61f0a064e4a66ec0ae24614 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 03:57:22 -0700 Subject: [PATCH 04/18] ts-loader update, correct css cell dimension error --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 16 +++++++++++----- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index b081fb1f..8d6a4f49 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -579,14 +579,20 @@ export class WebglRenderer extends Disposable implements IRenderer { this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio); - // Get the CSS dimensions of an individual cell. - this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._devicePixelRatio; - this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._devicePixelRatio; + // Get the CSS dimensions of an individual cell. This needs to be derived from the calculated + // device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the + // page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char + // size on the canvas can differ. + this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; + this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; } private _setCanvasDevicePixelDimensions(width: number, height: number): void { - this.dimensions.scaledCanvasHeight = width; - this.dimensions.scaledCanvasWidth = height; + if (this.dimensions.scaledCanvasWidth === width && this.dimensions.scaledCanvasHeight === height) { + return; + } + this.dimensions.scaledCanvasWidth = width; + this.dimensions.scaledCanvasHeight = height; this._canvas.width = width; this._canvas.height = height; this._requestRedrawViewport(); diff --git a/package.json b/package.json index 9db4d16f..a270a52f 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "playwright": "^1.22.1", "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", - "ts-loader": "^9.1.2", + "ts-loader": "^9.3.1", "typescript": "4.7", "utf8": "^3.0.0", "webpack": "^5.61.0", diff --git a/yarn.lock b/yarn.lock index 2a5c02bb..a1e6391c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3746,10 +3746,10 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" -ts-loader@^9.1.2: - version "9.2.6" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74" - integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw== +ts-loader@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.1.tgz#fe25cca56e3e71c1087fe48dc67f4df8c59b22d4" + integrity sha512-OkyShkcZTsTwyS3Kt7a4rsT/t2qvEVQuKCTg4LJmpj9fhFR7ukGdZwV6Qq3tRUkqcXtfGpPR7+hFKHCG/0d3Lw== dependencies: chalk "^4.1.0" enhanced-resolve "^5.0.0" From 47b1fdd14725811c85a9e52f67c9dd6aba362093 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 04:09:34 -0700 Subject: [PATCH 05/18] Refactor to share observeDevicePixelDimensions --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 27 ++------------- src/browser/renderer/DevicePixelObserver.ts | 34 +++++++++++++++++++ src/browser/renderer/Renderer.ts | 27 ++------------- 3 files changed, 38 insertions(+), 50 deletions(-) create mode 100644 src/browser/renderer/DevicePixelObserver.ts diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 8d6a4f49..b21f8c2f 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -16,6 +16,7 @@ import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'co import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; import { ITerminal, IColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { CellData } from 'common/buffer/CellData'; @@ -95,31 +96,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); })); - - // Observe any resizes to the canvas and extract the actual pixel size of the canvas if the - // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when - // converting between CSS pixels and device pixels which causes blurry rendering when device - // pixel ratio is not a round number. - let observer: ResizeObserver | undefined = new ResizeObserver((entries) => { - const entry = entries.find((entry) => entry.target === this._canvas); - if (!entry) { - return; - } - - // Disconnect if devicePixelContentBoxSize isn't supported by the browser - if (!('devicePixelContentBoxSize' in entry)) { - observer?.disconnect(); - observer = undefined; - return; - } - - this._setCanvasDevicePixelDimensions( - entry.devicePixelContentBoxSize[0].inlineSize, - entry.devicePixelContentBoxSize[0].blockSize - ); - }); - observer.observe(this._canvas, { box: ['device-pixel-content-box'] } as any); - this.register(toDisposable(() => observer?.disconnect())); + this.register(observeDevicePixelDimensions(this._canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); this._core.screenElement!.appendChild(this._canvas); diff --git a/src/browser/renderer/DevicePixelObserver.ts b/src/browser/renderer/DevicePixelObserver.ts new file mode 100644 index 00000000..611eb910 --- /dev/null +++ b/src/browser/renderer/DevicePixelObserver.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { toDisposable } from 'common/Lifecycle'; +import { IDisposable } from 'common/Types'; + +export function observeDevicePixelDimensions(element: HTMLElement, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { + // Observe any resizes to the element and extract the actual pixel size of the element if the + // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when + // converting between CSS pixels and device pixels which causes blurry rendering when device + // pixel ratio is not a round number. + let observer: ResizeObserver | undefined = new ResizeObserver((entries) => { + const entry = entries.find((entry) => entry.target === element); + if (!entry) { + return; + } + + // Disconnect if devicePixelContentBoxSize isn't supported by the browser + if (!('devicePixelContentBoxSize' in entry)) { + observer?.disconnect(); + observer = undefined; + return; + } + + callback( + entry.devicePixelContentBoxSize[0].inlineSize, + entry.devicePixelContentBoxSize[0].blockSize + ); + }); + observer.observe(element, { box: ['device-pixel-content-box'] } as any); + return toDisposable(() => observer?.disconnect()); +} diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index 04fdec77..45b7f1c1 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -14,6 +14,7 @@ import { ICharSizeService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services'; import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; let nextRendererId = 1; @@ -63,31 +64,7 @@ export class Renderer extends Disposable implements IRenderer { this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); - // Observe any resizes to the canvas and extract the actual pixel size of the canvas if the - // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when - // converting between CSS pixels and device pixels which causes blurry rendering when device - // pixel ratio is not a round number. - const observedCanvas = this._renderLayers[0].canvas; - let observer: ResizeObserver | undefined = new ResizeObserver((entries) => { - const entry = entries.find((entry) => entry.target === observedCanvas); - if (!entry) { - return; - } - - // Disconnect if devicePixelContentBoxSize isn't supported by the browser - if (!('devicePixelContentBoxSize' in entry)) { - observer?.disconnect(); - observer = undefined; - return; - } - - this._setCanvasDevicePixelDimensions( - entry.devicePixelContentBoxSize[0].inlineSize, - entry.devicePixelContentBoxSize[0].blockSize - ); - }); - observer.observe(observedCanvas, { box: ['device-pixel-content-box'] } as any); - this.register(toDisposable(() => observer?.disconnect())); + this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); this.onOptionsChanged(); } From 0165c3612dac9426607a1a70ab3899245dc65fc5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 07:11:56 -0700 Subject: [PATCH 06/18] Fix canvas device pixel dimension setting --- src/browser/renderer/Renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index 45b7f1c1..fe3c4c58 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -187,8 +187,8 @@ export class Renderer extends Disposable implements IRenderer { } private _setCanvasDevicePixelDimensions(width: number, height: number): void { - this.dimensions.scaledCanvasHeight = width; - this.dimensions.scaledCanvasWidth = height; + this.dimensions.scaledCanvasHeight = height; + this.dimensions.scaledCanvasWidth = width; // Resize all render layers for (const l of this._renderLayers) { l.resize(this.dimensions); From 0ccf3beec7cfddc1fdce5a6ca50ffd64f733e39c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 08:03:27 -0700 Subject: [PATCH 07/18] Retain hue when flipping luminance Fixes #3845 --- src/common/Color.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/Color.ts b/src/common/Color.ts index e5c7e3eb..f62dd6cb 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -195,7 +195,7 @@ export namespace rgba { const resultA = reduceLuminance(bgRgba, fgRgba, ratio); const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); if (resultARatio < ratio) { - const resultB = increaseLuminance(bgRgba, bgRgba, ratio); + const resultB = increaseLuminance(bgRgba, fgRgba, ratio); const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); return resultARatio > resultBRatio ? resultA : resultB; } @@ -204,7 +204,7 @@ export namespace rgba { const resultA = increaseLuminance(bgRgba, fgRgba, ratio); const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); if (resultARatio < ratio) { - const resultB = reduceLuminance(bgRgba, bgRgba, ratio); + const resultB = reduceLuminance(bgRgba, fgRgba, ratio); const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); return resultARatio > resultBRatio ? resultA : resultB; } From a39c7fb6368ad4908fd3b6b003a0991517a13dd6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 08:30:35 -0700 Subject: [PATCH 08/18] Never clear color when allowTransparency is on It's redundant and wasn't working as expected anyway. Fixes #3930 --- .../src/atlas/WebglCharAtlas.ts | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 05751c42..67ddb581 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -395,15 +395,15 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); } - // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible, - // try for a maximum of 5 pixels. + // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible + // even on the bottom row, try for a maximum of 5 pixels. if (chars === '_' && !this._config.allowTransparency) { - let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency); + let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor); if (isBeyondCellBounds) { for (let offset = 1; offset <= 5; offset++) { this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset); - isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency); + isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor); if (!isBeyondCellBounds) { break; } @@ -439,7 +439,12 @@ export class WebglCharAtlas implements IDisposable { ); // Clear out the background color and determine if the glyph is empty. - const isEmpty = clearColor(imageData, backgroundColor, foregroundColor, this._config.allowTransparency); + let isEmpty: boolean; + if (!this._config.allowTransparency) { + isEmpty = clearColor(imageData, backgroundColor, foregroundColor); + } else { + isEmpty = checkCompletelyTransparent(imageData); + } // Handle empty glyphs if (isEmpty) { @@ -583,7 +588,7 @@ export class WebglCharAtlas implements IDisposable { * transparent. * @returns True if the result is "empty", meaning all pixels are fully transparent. */ -function clearColor(imageData: ImageData, bg: IColor, fg: IColor, allowTransparency: boolean): boolean { +function clearColor(imageData: ImageData, bg: IColor, fg: IColor): boolean { // Get color channels const r = bg.rgba >>> 24; const g = bg.rgba >>> 16 & 0xFF; @@ -603,15 +608,14 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, allowTranspare // Set alpha channel of relevent pixels to 0 let isEmpty = true; for (let offset = 0; offset < imageData.data.length; offset += 4) { + // Check exact match if (imageData.data[offset] === r && imageData.data[offset + 1] === g && imageData.data[offset + 2] === b) { imageData.data[offset + 3] = 0; } else { - // Check the threshold only when transparency is not allowed only as overlapping isn't an - // issue for transparency glyphs. - if (!allowTransparency && - (Math.abs(imageData.data[offset] - r) + + // Check the threshold based difference + if ((Math.abs(imageData.data[offset] - r) + Math.abs(imageData.data[offset + 1] - g) + Math.abs(imageData.data[offset + 2] - b)) < threshold) { imageData.data[offset + 3] = 0; @@ -624,6 +628,15 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, allowTranspare return isEmpty; } +function checkCompletelyTransparent(imageData: ImageData): boolean { + for (let offset = 0; offset < imageData.data.length; offset += 4) { + if (imageData.data[offset + 3] > 0) { + return false; + } + } + return true; +} + function toPaddedHex(c: number): string { const s = c.toString(16); return s.length < 2 ? '0' + s : s; From 9acc36f8c034b996052d525e47de96212fc70c01 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 16:52:11 -0700 Subject: [PATCH 09/18] Warn when ligature fonts fail to download Fixes #3784 --- addons/xterm-addon-ligatures/bin/download-fonts.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-ligatures/bin/download-fonts.js b/addons/xterm-addon-ligatures/bin/download-fonts.js index ec4e5fea..27e75c09 100644 --- a/addons/xterm-addon-ligatures/bin/download-fonts.js +++ b/addons/xterm-addon-ligatures/bin/download-fonts.js @@ -22,10 +22,13 @@ const fontsFolder = path.join(__dirname, '../fonts'); async function download() { await mkdirp(fontsFolder); - await downloadFiraCode(); - await downloadIosevka(); - - console.log('Loaded all fonts for testing') + try { + await downloadFiraCode(); + await downloadIosevka(); + console.log('Loaded all fonts for testing') + } catch (e) { + console.warn('Fonts failed to download, ligature tests will not work', e); + } } async function downloadFiraCode() { From 3322418728faaeb2070053ea37595d5aad8f37ba Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Jul 2022 17:45:14 -0700 Subject: [PATCH 10/18] Remove shift+backspace -> ^H binding This has been there since the first commit of term.js, it's not clear why it's there but Terminal.app, iTerm2 and kitty seem to just do a regular backspace Fixes #3759 --- src/common/input/Keyboard.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index 0481ad99..013a7711 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -83,10 +83,7 @@ export function evaluateKeyboardEvent( break; case 8: // backspace - if (ev.shiftKey) { - result.key = C0.BS; // ^H - break; - } else if (ev.altKey) { + if (ev.altKey) { result.key = C0.ESC + C0.DEL; // \e ^? break; } From 9159fc68e163f902f9993d6c8bf97a39d39cf1e8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 Jul 2022 09:44:54 -0700 Subject: [PATCH 11/18] Redraw canvas renderer selection on resize Fixes #1963 --- src/browser/renderer/BaseRenderLayer.ts | 6 +++--- src/browser/renderer/SelectionRenderLayer.ts | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 0a9b8057..a44253a0 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -29,9 +29,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - private _selectionStart: [number, number] | undefined; - private _selectionEnd: [number, number] | undefined; - private _columnSelectMode: boolean = false; + protected _selectionStart: [number, number] | undefined; + protected _selectionEnd: [number, number] | undefined; + protected _columnSelectMode: boolean = false; protected _charAtlas: BaseCharAtlas | undefined; diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index ce4fe071..f8bdc234 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -42,8 +42,11 @@ export class SelectionRenderLayer extends BaseRenderLayer { public resize(dim: IRenderDimensions): void { super.resize(dim); - // Resizing the canvas discards the contents of the canvas so clear state - this._clearState(); + // On resize use the base render layer's cached selection values since resize clears _state + // inside reset. + if (this._selectionStart && this._selectionEnd) { + this.onSelectionChanged(this._selectionStart, this._selectionEnd, this._columnSelectMode); + } } public reset(): void { From a55728dd28f45f3da643aa38196a616e1334fcb1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:41:00 -0700 Subject: [PATCH 12/18] Add duration-based smooth scroll Fixes #1140 --- src/browser/Viewport.ts | 50 ++++++++++++++++++++++++++- src/common/services/OptionsService.ts | 1 + src/common/services/Services.ts | 1 + typings/xterm-headless.d.ts | 9 ++++- typings/xterm.d.ts | 6 ++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 1eb9dc4e..65ccc333 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -36,6 +36,9 @@ export class Viewport extends Disposable implements IViewport { private _refreshAnimationFrame: number | null = null; private _ignoreNextScrollEvent: boolean = false; + private _lastSmoothScrollOrigin?: number = undefined; + private _lastSmoothScrollTarget?: number = undefined; + private _lastSmoothScrollStartTime?: number = undefined; constructor( private readonly _scrollLines: (amount: number) => void, @@ -168,6 +171,33 @@ export class Viewport extends Disposable implements IViewport { this._scrollLines(diff); } + private _smoothScroll(): void { + // Check valid state + if (this._isDisposed || this._lastSmoothScrollOrigin === undefined || this._lastSmoothScrollTarget === undefined) { + return; + } + + // Calculate position complete + const percent = this._smoothScrollPercent(); + this._viewportElement.scrollTop = this._lastSmoothScrollOrigin + Math.round(percent * (this._lastSmoothScrollTarget - this._lastSmoothScrollOrigin)); + + // Continue or finish smooth scroll + if (percent < 1) { + window.requestAnimationFrame(() => this._smoothScroll()); + } else { + this._lastSmoothScrollStartTime = undefined; + this._lastSmoothScrollOrigin = undefined; + this._lastSmoothScrollTarget = undefined; + } + } + + private _smoothScrollPercent(): number { + if (!this._optionsService.rawOptions.smoothScrollingDuration || !this._lastSmoothScrollStartTime) { + return 1; + } + return Math.max(Math.min((Date.now() - this._lastSmoothScrollStartTime) / this._optionsService.rawOptions.smoothScrollingDuration, 1), 0); + } + /** * Handles bubbling of scroll event in case the viewport has reached top or bottom * @param ev The scroll event. @@ -196,7 +226,25 @@ export class Viewport extends Disposable implements IViewport { if (amount === 0) { return false; } - this._viewportElement.scrollTop += amount; + if (!this._optionsService.rawOptions.smoothScrollingDuration) { + this._viewportElement.scrollTop += amount; + } else { + this._lastSmoothScrollStartTime = Date.now(); + if (this._smoothScrollPercent() < 1) { + this._lastSmoothScrollOrigin = this._viewportElement.scrollTop; + if (this._lastSmoothScrollTarget === undefined) { + this._lastSmoothScrollTarget = this._viewportElement.scrollTop + amount; + } else { + this._lastSmoothScrollTarget += amount; + } + this._lastSmoothScrollTarget = Math.max(Math.min(this._lastSmoothScrollTarget, this._viewportElement.scrollHeight), 0); + this._smoothScroll(); + } else { + this._lastSmoothScrollStartTime = undefined; + this._lastSmoothScrollOrigin = undefined; + this._lastSmoothScrollTarget = undefined; + } + } return this._bubbleScroll(ev, amount); } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 4f9600a4..32358bdd 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,6 +37,7 @@ export const DEFAULT_OPTIONS: Readonly = { scrollback: 1000, scrollSensitivity: 1, screenReaderMode: false, + smoothScrollingDuration: 125, macOptionIsMeta: false, macOptionClickForcesSelection: false, minimumContrastRatio: 1, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index fab8435a..72418ee2 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -241,6 +241,7 @@ export interface ITerminalOptions { screenReaderMode: boolean; scrollback: number; scrollSensitivity: number; + smoothScrollingDuration: number; tabStopWidth: number; theme: ITheme; windowsMode: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 26a01e4c..cc08039e 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -182,10 +182,17 @@ declare module 'xterm-headless' { scrollback?: number; /** - * The scrolling speed multiplier used for adjusting normal scrolling speed. + * The duration to smoothly scroll between the origin and the target in + * milliseconds. Set to 0 to disable smooth scrolling and scroll instantly. */ scrollSensitivity?: number; + /** + * The duration to smoothly scroll between the origin and the target. Set + * this to 0 to disable smooth scrolling and scroll instantly. + */ + smoothScrollingDuration?: number; + /** * The size of tab stops in the terminal. */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a3f47300..afa138d7 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -233,6 +233,12 @@ declare module 'xterm' { */ scrollSensitivity?: number; + /** + * The duration to smoothly scroll between the origin and the target in + * milliseconds. Set to 0 to disable smooth scrolling and scroll instantly. + */ + smoothScrollingDuration?: number; + /** * The size of tab stops in the terminal. */ From fb4cb0f140a42d61359048eded4b74aa14645c84 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:44:34 -0700 Subject: [PATCH 13/18] Reduce smooth scroll GC --- src/browser/Viewport.ts | 48 +++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 65ccc333..c1afabd3 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -13,6 +13,12 @@ import { IRenderDimensions } from 'browser/renderer/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; +interface ISmoothScrollState { + startTime: number; + origin: number; + target: number; +} + /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. * Logic for the virtual scroll bar is included in this object. @@ -36,9 +42,11 @@ export class Viewport extends Disposable implements IViewport { private _refreshAnimationFrame: number | null = null; private _ignoreNextScrollEvent: boolean = false; - private _lastSmoothScrollOrigin?: number = undefined; - private _lastSmoothScrollTarget?: number = undefined; - private _lastSmoothScrollStartTime?: number = undefined; + private _smoothScrollState: ISmoothScrollState = { + startTime: 0, + origin: -1, + target: -1 + }; constructor( private readonly _scrollLines: (amount: number) => void, @@ -173,29 +181,33 @@ export class Viewport extends Disposable implements IViewport { private _smoothScroll(): void { // Check valid state - if (this._isDisposed || this._lastSmoothScrollOrigin === undefined || this._lastSmoothScrollTarget === undefined) { + if (this._isDisposed || this._smoothScrollState.origin === -1 || this._smoothScrollState.target === -1) { return; } // Calculate position complete const percent = this._smoothScrollPercent(); - this._viewportElement.scrollTop = this._lastSmoothScrollOrigin + Math.round(percent * (this._lastSmoothScrollTarget - this._lastSmoothScrollOrigin)); + this._viewportElement.scrollTop = this._smoothScrollState.origin + Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin)); // Continue or finish smooth scroll if (percent < 1) { window.requestAnimationFrame(() => this._smoothScroll()); } else { - this._lastSmoothScrollStartTime = undefined; - this._lastSmoothScrollOrigin = undefined; - this._lastSmoothScrollTarget = undefined; + this._clearSmoothScrollState(); } } private _smoothScrollPercent(): number { - if (!this._optionsService.rawOptions.smoothScrollingDuration || !this._lastSmoothScrollStartTime) { + if (!this._optionsService.rawOptions.smoothScrollingDuration || !this._smoothScrollState.startTime) { return 1; } - return Math.max(Math.min((Date.now() - this._lastSmoothScrollStartTime) / this._optionsService.rawOptions.smoothScrollingDuration, 1), 0); + return Math.max(Math.min((Date.now() - this._smoothScrollState.startTime) / this._optionsService.rawOptions.smoothScrollingDuration, 1), 0); + } + + private _clearSmoothScrollState(): void { + this._smoothScrollState.startTime = 0; + this._smoothScrollState.origin = -1; + this._smoothScrollState.target = -1; } /** @@ -229,20 +241,18 @@ export class Viewport extends Disposable implements IViewport { if (!this._optionsService.rawOptions.smoothScrollingDuration) { this._viewportElement.scrollTop += amount; } else { - this._lastSmoothScrollStartTime = Date.now(); + this._smoothScrollState.startTime = Date.now(); if (this._smoothScrollPercent() < 1) { - this._lastSmoothScrollOrigin = this._viewportElement.scrollTop; - if (this._lastSmoothScrollTarget === undefined) { - this._lastSmoothScrollTarget = this._viewportElement.scrollTop + amount; + this._smoothScrollState.origin = this._viewportElement.scrollTop; + if (this._smoothScrollState.target === -1) { + this._smoothScrollState.target = this._viewportElement.scrollTop + amount; } else { - this._lastSmoothScrollTarget += amount; + this._smoothScrollState.target += amount; } - this._lastSmoothScrollTarget = Math.max(Math.min(this._lastSmoothScrollTarget, this._viewportElement.scrollHeight), 0); + this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0); this._smoothScroll(); } else { - this._lastSmoothScrollStartTime = undefined; - this._lastSmoothScrollOrigin = undefined; - this._lastSmoothScrollTarget = undefined; + this._clearSmoothScrollState(); } } return this._bubbleScroll(ev, amount); From 4b8e7a57683a2c39e1b838ed59697850593185c0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:45:56 -0700 Subject: [PATCH 14/18] Change default to 0 --- src/browser/Viewport.ts | 6 +++--- src/common/services/OptionsService.ts | 2 +- src/common/services/Services.ts | 2 +- typings/xterm-headless.d.ts | 2 +- typings/xterm.d.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index c1afabd3..6cff1f98 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -198,10 +198,10 @@ export class Viewport extends Disposable implements IViewport { } private _smoothScrollPercent(): number { - if (!this._optionsService.rawOptions.smoothScrollingDuration || !this._smoothScrollState.startTime) { + if (!this._optionsService.rawOptions.smoothScrollDuration || !this._smoothScrollState.startTime) { return 1; } - return Math.max(Math.min((Date.now() - this._smoothScrollState.startTime) / this._optionsService.rawOptions.smoothScrollingDuration, 1), 0); + return Math.max(Math.min((Date.now() - this._smoothScrollState.startTime) / this._optionsService.rawOptions.smoothScrollDuration, 1), 0); } private _clearSmoothScrollState(): void { @@ -238,7 +238,7 @@ export class Viewport extends Disposable implements IViewport { if (amount === 0) { return false; } - if (!this._optionsService.rawOptions.smoothScrollingDuration) { + if (!this._optionsService.rawOptions.smoothScrollDuration) { this._viewportElement.scrollTop += amount; } else { this._smoothScrollState.startTime = Date.now(); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 32358bdd..87d04ff9 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,7 +37,7 @@ export const DEFAULT_OPTIONS: Readonly = { scrollback: 1000, scrollSensitivity: 1, screenReaderMode: false, - smoothScrollingDuration: 125, + smoothScrollDuration: 0, macOptionIsMeta: false, macOptionClickForcesSelection: false, minimumContrastRatio: 1, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 72418ee2..15cfd8ea 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -241,7 +241,7 @@ export interface ITerminalOptions { screenReaderMode: boolean; scrollback: number; scrollSensitivity: number; - smoothScrollingDuration: number; + smoothScrollDuration: number; tabStopWidth: number; theme: ITheme; windowsMode: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index cc08039e..85433829 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -191,7 +191,7 @@ declare module 'xterm-headless' { * The duration to smoothly scroll between the origin and the target. Set * this to 0 to disable smooth scrolling and scroll instantly. */ - smoothScrollingDuration?: number; + smoothScrollDuration?: number; /** * The size of tab stops in the terminal. diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index afa138d7..1413f66b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -237,7 +237,7 @@ declare module 'xterm' { * The duration to smoothly scroll between the origin and the target in * milliseconds. Set to 0 to disable smooth scrolling and scroll instantly. */ - smoothScrollingDuration?: number; + smoothScrollDuration?: number; /** * The size of tab stops in the terminal. From 78bfe0b89d5a1556f693b42257536c9d9dac6da6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:47:39 -0700 Subject: [PATCH 15/18] Revert accidental setting change --- typings/xterm-headless.d.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 85433829..07c1749d 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -182,16 +182,15 @@ declare module 'xterm-headless' { scrollback?: number; /** - * The duration to smoothly scroll between the origin and the target in - * milliseconds. Set to 0 to disable smooth scrolling and scroll instantly. + * The scrolling speed multiplier used for adjusting normal scrolling speed. */ scrollSensitivity?: number; /** - * The duration to smoothly scroll between the origin and the target. Set - * this to 0 to disable smooth scrolling and scroll instantly. + * The duration to smoothly scroll between the origin and the target in + * milliseconds. Set to 0 to disable smooth scrolling and scroll instantly. */ - smoothScrollDuration?: number; + smoothScrollDuration?: number; /** * The size of tab stops in the terminal. From 0b0662c4d9d5bb0cd60f84dfab0c3e697003e8e6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 07:27:27 -0700 Subject: [PATCH 16/18] xterm-addon-canvas@0.1.0 --- addons/xterm-addon-canvas/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-canvas/package.json b/addons/xterm-addon-canvas/package.json index 3d08e9db..ba19e674 100644 --- a/addons/xterm-addon-canvas/package.json +++ b/addons/xterm-addon-canvas/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-canvas", - "version": "0.12.0", + "version": "0.1.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" From 1be14e4338cf174740414752ad1d7e9770f143c9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 09:09:44 -0700 Subject: [PATCH 17/18] Fix version of published addons --- bin/publish.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/publish.js b/bin/publish.js index cb6c836a..78ebde13 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -98,9 +98,13 @@ function getNextBetaVersion(packageJson) { process.exit(1); } const tag = 'beta'; - // const stableVersion = packageJson.version.split('.'); - // const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; - const nextStableVersion = `5.0.0`; + let nextStableVersion; + if (packageJson.name = 'xterm') { + nextStableVersion = `5.0.0`; + } else { + const stableVersion = packageJson.version.split('.'); + nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; + } const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); if (publishedVersions.length === 0) { return `${nextStableVersion}-${tag}.1`; From d9880c0270363d58a50a1b1f95a27571dfea19f4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 10:20:57 -0700 Subject: [PATCH 18/18] Fix publishing addons as xterm --- bin/publish.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/publish.js b/bin/publish.js index cb6c836a..6b5abcd9 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -98,9 +98,13 @@ function getNextBetaVersion(packageJson) { process.exit(1); } const tag = 'beta'; - // const stableVersion = packageJson.version.split('.'); - // const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; - const nextStableVersion = `5.0.0`; + let nextStableVersion; + if (packageJson.name === 'xterm') { + nextStableVersion = `5.0.0`; + } else { + const stableVersion = packageJson.version.split('.'); + nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; + } const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); if (publishedVersions.length === 0) { return `${nextStableVersion}-${tag}.1`;