diff --git a/addons/addon-webgl/src/Types.ts b/addons/addon-webgl/src/Types.ts index 67c9408d6c..9a4fab07e8 100644 --- a/addons/addon-webgl/src/Types.ts +++ b/addons/addon-webgl/src/Types.ts @@ -40,7 +40,7 @@ export interface ICharAtlasConfig { devicePixelRatio: number; deviceMaxTextureSize: number; letterSpacing: number; - lineHeight: number; + lineHeight: number | string; fontSize: number; fontFamily: string; fontWeight: FontWeight; diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index e985bf2efa..ef48dcc7a2 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -623,14 +623,17 @@ export class WebglRenderer extends Disposable implements IRenderer { // cell. this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio); - // Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be - // floored since lineHeight can never be lower then 1, this guarentees the device cell height - // will always be larger than device char height. - this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight); + // Calculate the device cell height, if lineHeight is _not_ 1 (or is a px string), the + // resulting value will be floored since lineHeight can never be lower than 1, this guarantees + // the device cell height will always be larger than device char height. + const lineHeight = this._optionsService.rawOptions.lineHeight; + this.dimensions.device.cell.height = typeof lineHeight === 'string' + ? Math.max(Math.floor(parseFloat(lineHeight) * this._devicePixelRatio), this.dimensions.device.char.height) + : Math.floor(this.dimensions.device.char.height * lineHeight); // 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.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2); + this.dimensions.device.char.top = this.dimensions.device.cell.height === this.dimensions.device.char.height ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2); // Calculate the device cell width, taking the letterSpacing into account. this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing); diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index c3d63b8e73..c285417ca1 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -133,7 +133,10 @@ export class DomRenderer extends Disposable implements IRenderer { this.dimensions.device.char.width = this._charSizeService.width * dpr; this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr); this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing); - this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight); + const lineHeight = this._optionsService.rawOptions.lineHeight; + this.dimensions.device.cell.height = typeof lineHeight === 'string' + ? Math.max(Math.floor(parseFloat(lineHeight) * dpr), this.dimensions.device.char.height) + : Math.floor(this.dimensions.device.char.height * lineHeight); this.dimensions.device.char.left = 0; this.dimensions.device.char.top = 0; this.dimensions.device.canvas.width = this.dimensions.device.cell.width * this._bufferService.cols; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ec647aaa50..e441de27f5 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -178,12 +178,26 @@ export class OptionsService extends Disposable implements IOptionsService { case 'cursorWidth': value = Math.floor(value); // Fall through for bounds check - case 'lineHeight': case 'tabStopWidth': if (value < 1) { throw new Error(`${key} cannot be less than 1, value: ${value}`); } break; + case 'lineHeight': + if (typeof value === 'string') { + if (!value.endsWith('px')) { + throw new Error(`${key} string value must end with 'px', value: ${value}`); + } + const parsed = parseFloat(value); + if (isNaN(parsed) || parsed < 1) { + throw new Error(`${key} must be a number >= 1 when using px format, value: ${value}`); + } + } else if (typeof value === 'number') { + if (value < 1) { + throw new Error(`${key} cannot be less than 1, value: ${value}`); + } + } + break; case 'minimumContrastRatio': value = Math.max(1, Math.min(21, Math.round(value * 10) / 10)); break; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e0af27eeed..f85d0268c2 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -243,7 +243,7 @@ export interface ITerminalOptions { fontWeightBold?: FontWeight; ignoreBracketedPasteMode?: boolean; letterSpacing?: number; - lineHeight?: number; + lineHeight?: number | string; linkHandler?: ILinkHandler | null; logLevel?: LogLevel; logger?: ILogger | null; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c8e08f509f..a0511675a0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -145,9 +145,12 @@ declare module '@xterm/xterm' { letterSpacing?: number; /** - * The line height used to render text. + * The line height used to render text. When a number is given, it is used + * as a multiplier of the character height (must be >= 1). When a string + * ending in `'px'` is given (e.g. `'23px'`), it sets an absolute pixel + * line height (the parsed value must be >= 1). */ - lineHeight?: number; + lineHeight?: number | string; /** * The handler for OSC 8 hyperlinks. Links will use the `confirm` browser