diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 14ff9433..c9349760 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -646,6 +646,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { if (deltaY === 0) { return false; } + const lines = self.coreMouseService.consumeWheelEvent( + ev as WheelEvent, + self._renderService?.dimensions?.device?.cell?.height, + self._coreBrowserService?.dpr + ); + if (lines === 0) { + return false; + } action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; but = CoreMouseButton.WHEEL; break; @@ -817,6 +825,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { return false; } + const lines = self.coreMouseService.consumeWheelEvent( + ev as WheelEvent, + self._renderService?.dimensions?.device?.cell?.height, + self._coreBrowserService?.dpr + ); + if (lines === 0) { + return false; + } + // Construct and send sequences const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B'); this.coreService.triggerDataEvent(sequence, true); diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index f504cbe9..8e3d02f9 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -71,6 +71,9 @@ export class MockCoreMouseService implements ICoreMouseService { public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } + public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number { + return 1; + } } export class MockCharsetService implements ICharsetService { diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index 34710897..3b79596d 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -3,13 +3,14 @@ * @license MIT */ import { CoreMouseService } from 'common/services/CoreMouseService'; -import { MockCoreService, MockBufferService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test'; import { assert } from 'chai'; import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; // needed mock services const bufferService = new MockBufferService(300, 100); const coreService = new MockCoreService(); +const optionsService = new MockOptionsService(); function toBytes(s: string | undefined): number[] { if (!s) { @@ -24,20 +25,20 @@ function toBytes(s: string | undefined): number[] { describe('CoreMouseService', () => { it('init', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); assert.equal(cms.activeEncoding, 'DEFAULT'); assert.equal(cms.activeProtocol, 'NONE'); }); it('default protocols - NONE, X10, VT200, DRAG, ANY', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']); }); it('default encodings - DEFAULT, SGR', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']); }); it('protocol/encoding setter, reset', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); cms.activeEncoding = 'SGR'; cms.activeProtocol = 'ANY'; assert.equal(cms.activeEncoding, 'SGR'); @@ -49,19 +50,19 @@ describe('CoreMouseService', () => { assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"'); }); it('addEncoding', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); cms.addEncoding('XYZ', (e: ICoreMouseEvent) => ''); cms.activeEncoding = 'XYZ'; assert.equal(cms.activeEncoding, 'XYZ'); }); it('addProtocol', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false }); cms.activeProtocol = 'XYZ'; assert.equal(cms.activeProtocol, 'XYZ'); }); it('onProtocolChange', () => { - const cms = new CoreMouseService(bufferService, coreService); + const cms = new CoreMouseService(bufferService, coreService, optionsService); const wantedEvents: CoreMouseEventType[] = []; cms.onProtocolChange(events => wantedEvents.push(events)); cms.activeProtocol = 'NONE'; @@ -76,7 +77,7 @@ describe('CoreMouseService', () => { let cms: CoreMouseService; let reports: string[]; beforeEach(() => { - cms = new CoreMouseService(bufferService, coreService); + cms = new CoreMouseService(bufferService, coreService, optionsService); reports = []; coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); coreService.triggerBinaryEvent = (data: string) => reports.push(data); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index f2f02379..a10ddafb 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -2,7 +2,7 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ -import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; +import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; @@ -174,13 +174,15 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { private _activeProtocol: string = ''; private _activeEncoding: string = ''; private _lastEvent: ICoreMouseEvent | null = null; + private _wheelPartialScroll: number = 0; private readonly _onProtocolChange = this._register(new Emitter()); - public readonly onProtocolChange = this._onProtocolChange.event; + public readonly onProtocolChange = this._onProtocolChange.event; constructor( @IBufferService private readonly _bufferService: IBufferService, - @ICoreService private readonly _coreService: ICoreService + @ICoreService private readonly _coreService: ICoreService, + @IOptionsService private readonly _optionsService: IOptionsService ) { super(); // register default protocols and encodings @@ -229,6 +231,49 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { this.activeProtocol = 'NONE'; this.activeEncoding = 'DEFAULT'; this._lastEvent = null; + this._wheelPartialScroll = 0; + } + + /** + * Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls. + * This prevents hyper-sensitive scrolling in alt buffer. + */ + public consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0 || ev.shiftKey) { + return 0; + } + + if (cellHeight === undefined || dpr === undefined) { + return 0; + } + + const targetWheelEventPixels = cellHeight / dpr; + let amount = this._applyScrollModifier(ev.deltaY, ev); + + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + amount /= (targetWheelEventPixels + 0.0); // Prevent integer division + + const isLikelyTrackpad = Math.abs(ev.deltaY) < 50; + if (isLikelyTrackpad) { + amount *= 0.3; + } + + 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._bufferService.rows; + } + return amount; + } + + private _applyScrollModifier(amount: number, ev: WheelEvent): number { + // Multiply the scroll speed when the modifier key is pressed + if (ev.altKey || ev.ctrlKey || ev.shiftKey) { + return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity; + } + return amount * this._optionsService.rawOptions.scrollSensitivity; } /** diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 9c3aebf7..d47c3bbc 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -58,6 +58,11 @@ export interface ICoreMouseService { * Human readable version of mouse events. */ explainEvents(events: CoreMouseEventType): { [event: string]: boolean }; + + /** + * Process wheel event taking partial scroll into account. + */ + consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number; } export const ICoreService = createDecorator('CoreService');