diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 83f20c9e7b..79e964aa42 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -623,14 +623,16 @@ 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. Values >= 8 are treated as absolute pixel heights + // (Monaco convention), values < 8 are treated as multipliers of the character height. + const lineHeight = this._optionsService.rawOptions.lineHeight; + this.dimensions.device.cell.height = lineHeight >= 8 + ? Math.max(Math.floor(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 08b5093dfb..e2dfd74f56 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -136,7 +136,12 @@ 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); + // Values >= 8 are treated as absolute pixel heights (Monaco convention), + // values < 8 are treated as multipliers of the character height. + const lineHeight = this._optionsService.rawOptions.lineHeight; + this.dimensions.device.cell.height = lineHeight >= 8 + ? Math.max(Math.floor(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.test.ts b/src/common/services/OptionsService.test.ts index 004f731687..832c5a1c7f 100644 --- a/src/common/services/OptionsService.test.ts +++ b/src/common/services/OptionsService.test.ts @@ -128,6 +128,36 @@ describe('OptionsService', () => { }); }); }); + describe('lineHeight', () => { + let service: OptionsService; + beforeEach(() => { + service = new OptionsService({}); + }); + it('should default to 1', () => { + assert.strictEqual(service.options.lineHeight, 1); + }); + it('should accept multiplier values (< 8)', () => { + service.options.lineHeight = 1.5; + assert.strictEqual(service.options.lineHeight, 1.5); + }); + it('should accept pixel values (>= 8)', () => { + service.options.lineHeight = 20; + assert.strictEqual(service.options.lineHeight, 20); + }); + it('should accept boundary value 8 as pixel height', () => { + service.options.lineHeight = 8; + assert.strictEqual(service.options.lineHeight, 8); + }); + it('should accept 7.9 as multiplier', () => { + service.options.lineHeight = 7.9; + assert.strictEqual(service.options.lineHeight, 7.9); + }); + it('should reject values less than 1', () => { + assert.throws(() => { service.options.lineHeight = 0.5; }); + assert.throws(() => { service.options.lineHeight = 0; }); + assert.throws(() => { service.options.lineHeight = -1; }); + }); + }); describe('onMultipleOptionChange', () => { let service: OptionsService; beforeEach(() => { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ec647aaa50..2ac335f54c 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -178,12 +178,18 @@ 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': + // Values >= 8 are treated as absolute pixel heights (Monaco convention), + // values < 8 are treated as multipliers and must be >= 1. + 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/typings/xterm.d.ts b/typings/xterm.d.ts index c8e08f509f..39b1a96003 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -145,7 +145,10 @@ declare module '@xterm/xterm' { letterSpacing?: number; /** - * The line height used to render text. + * The line height used to render text. When the value is less than 8, it is + * treated as a multiplier of the character height (e.g. `1.2`). When the + * value is 8 or greater, it is treated as an absolute pixel height. This + * follows the same convention as Monaco editor's `lineHeight` option. */ lineHeight?: number;