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. */