diff --git a/src/Terminal.ts b/src/Terminal.ts index 02dc261d..735711d0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1052,7 +1052,27 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // } on(el, 'wheel', (ev: WheelEvent) => { - if (!this.mouseEvents) return; + if (!this.mouseEvents) { + // Convert wheel events into up/down events when the buffer does not have scrollback, this + // enables scrolling in apps hosted in the alt buffer such as vim or tmux. + if (!this.buffer.hasScrollback) { + const amount = this.viewport.getLinesScrolled(ev); + + // Do nothing if there's no vertical scroll + if (amount === 0) { + return; + } + + // Construct and send sequences + const sequence = C0.ESC + (this.applicationCursor ? 'O' : '[') + ( ev.deltaY < 0 ? 'A' : 'B'); + let data = ''; + for (let i = 0; i < Math.abs(amount); i++) { + data += sequence; + } + this.send(data); + } + return; + } if (this.x10Mouse || this._vt300Mouse || this._decLocator) return; sendButton(ev); ev.preventDefault(); diff --git a/src/Types.ts b/src/Types.ts index 6bc8b963..2751d266 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -89,6 +89,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { export interface IViewport { scrollBarWidth: number; syncScrollArea(): void; + getLinesScrolled(ev: WheelEvent): number; onWheel(ev: WheelEvent): void; onTouchStart(ev: TouchEvent): void; onTouchMove(ev: TouchEvent): void; diff --git a/src/Viewport.ts b/src/Viewport.ts index 59d95406..4d135b5a 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -21,6 +21,11 @@ export class Viewport implements IViewport { private _lastRecordedBufferHeight: number = 0; private _lastTouchY: number; + // Stores a partial line amount when scrolling, this is used to keep track of how much of a line + // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a + // quick fix and could have a more robust solution in place that reset the value when needed. + private _wheelPartialScroll: number = 0; + /** * Creates a new Viewport. * @param _terminal The terminal this viewport belongs to. @@ -113,22 +118,55 @@ export class Viewport implements IViewport { * @param ev The mouse wheel event. */ public onWheel(ev: WheelEvent): void { - if (ev.deltaY === 0) { - // Do nothing if it's not a vertical scroll event + const amount = this._getPixelsScrolled(ev); + if (amount === 0) { return; } - // Fallback to WheelEvent.DOM_DELTA_PIXEL - let multiplier = 1; - if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { - multiplier = this._currentRowHeight; - } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { - multiplier = this._currentRowHeight * this._terminal.rows; - } - this._viewportElement.scrollTop += ev.deltaY * multiplier; + this._viewportElement.scrollTop += amount; // Prevent the page from scrolling when the terminal scrolls ev.preventDefault(); } + private _getPixelsScrolled(ev: WheelEvent): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0) { + return 0; + } + + // Fallback to WheelEvent.DOM_DELTA_PIXEL + let amount = ev.deltaY; + if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { + amount *= this._currentRowHeight; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + amount *= this._currentRowHeight * this._terminal.rows; + } + return amount; + } + + /** + * Gets the number of pixels scrolled by the mouse event taking into account what type of delta + * is being used. + * @param ev The mouse wheel event. + */ + public getLinesScrolled(ev: WheelEvent): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0) { + return 0; + } + + // Fallback to WheelEvent.DOM_DELTA_LINE + let amount = ev.deltaY; + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + amount /= this._currentRowHeight + 0.0; // Prevent integer division + this._wheelPartialScroll += amount; + amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1); + this._wheelPartialScroll %= 1; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + amount *= this._terminal.rows; + } + return amount; + } + /** * Handles the touchstart event, recording the touch occurred. * @param ev The touch event. diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 2d9be43d..c1ab2115 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -338,6 +338,9 @@ export class MockViewport implements IViewport { throw new Error('Method not implemented.'); } syncScrollArea(): void { } + getLinesScrolled(ev: WheelEvent): number { + throw new Error('Method not implemented.'); + } } export class MockCompositionHelper implements ICompositionHelper {