Skip to content

Commit d7d7f1b

Browse files
authored
Merge pull request #5391 from Tyriar/tyriar/mouse_events
Bring back partial wheel tracking
2 parents 0d1e6b7 + 9c42fb3 commit d7d7f1b

File tree

5 files changed

+83
-12
lines changed

5 files changed

+83
-12
lines changed

src/browser/CoreBrowserTerminal.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
646646
if (deltaY === 0) {
647647
return false;
648648
}
649+
const lines = self.coreMouseService.consumeWheelEvent(
650+
ev as WheelEvent,
651+
self._renderService?.dimensions?.device?.cell?.height,
652+
self._coreBrowserService?.dpr
653+
);
654+
if (lines === 0) {
655+
return false;
656+
}
649657
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
650658
but = CoreMouseButton.WHEEL;
651659
break;
@@ -817,6 +825,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
817825
return false;
818826
}
819827

828+
const lines = self.coreMouseService.consumeWheelEvent(
829+
ev as WheelEvent,
830+
self._renderService?.dimensions?.device?.cell?.height,
831+
self._coreBrowserService?.dpr
832+
);
833+
if (lines === 0) {
834+
return false;
835+
}
836+
820837
// Construct and send sequences
821838
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
822839
this.coreService.triggerDataEvent(sequence, true);

src/common/TestUtils.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export class MockCoreMouseService implements ICoreMouseService {
7171
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
7272
throw new Error('Method not implemented.');
7373
}
74+
public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number {
75+
return 1;
76+
}
7477
}
7578

7679
export class MockCharsetService implements ICharsetService {

src/common/services/CoreMouseService.test.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
* @license MIT
44
*/
55
import { CoreMouseService } from 'common/services/CoreMouseService';
6-
import { MockCoreService, MockBufferService } from 'common/TestUtils.test';
6+
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
77
import { assert } from 'chai';
88
import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
99

1010
// needed mock services
1111
const bufferService = new MockBufferService(300, 100);
1212
const coreService = new MockCoreService();
13+
const optionsService = new MockOptionsService();
1314

1415
function toBytes(s: string | undefined): number[] {
1516
if (!s) {
@@ -24,20 +25,20 @@ function toBytes(s: string | undefined): number[] {
2425

2526
describe('CoreMouseService', () => {
2627
it('init', () => {
27-
const cms = new CoreMouseService(bufferService, coreService);
28+
const cms = new CoreMouseService(bufferService, coreService, optionsService);
2829
assert.equal(cms.activeEncoding, 'DEFAULT');
2930
assert.equal(cms.activeProtocol, 'NONE');
3031
});
3132
it('default protocols - NONE, X10, VT200, DRAG, ANY', () => {
32-
const cms = new CoreMouseService(bufferService, coreService);
33+
const cms = new CoreMouseService(bufferService, coreService, optionsService);
3334
assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']);
3435
});
3536
it('default encodings - DEFAULT, SGR', () => {
36-
const cms = new CoreMouseService(bufferService, coreService);
37+
const cms = new CoreMouseService(bufferService, coreService, optionsService);
3738
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']);
3839
});
3940
it('protocol/encoding setter, reset', () => {
40-
const cms = new CoreMouseService(bufferService, coreService);
41+
const cms = new CoreMouseService(bufferService, coreService, optionsService);
4142
cms.activeEncoding = 'SGR';
4243
cms.activeProtocol = 'ANY';
4344
assert.equal(cms.activeEncoding, 'SGR');
@@ -49,19 +50,19 @@ describe('CoreMouseService', () => {
4950
assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
5051
});
5152
it('addEncoding', () => {
52-
const cms = new CoreMouseService(bufferService, coreService);
53+
const cms = new CoreMouseService(bufferService, coreService, optionsService);
5354
cms.addEncoding('XYZ', (e: ICoreMouseEvent) => '');
5455
cms.activeEncoding = 'XYZ';
5556
assert.equal(cms.activeEncoding, 'XYZ');
5657
});
5758
it('addProtocol', () => {
58-
const cms = new CoreMouseService(bufferService, coreService);
59+
const cms = new CoreMouseService(bufferService, coreService, optionsService);
5960
cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false });
6061
cms.activeProtocol = 'XYZ';
6162
assert.equal(cms.activeProtocol, 'XYZ');
6263
});
6364
it('onProtocolChange', () => {
64-
const cms = new CoreMouseService(bufferService, coreService);
65+
const cms = new CoreMouseService(bufferService, coreService, optionsService);
6566
const wantedEvents: CoreMouseEventType[] = [];
6667
cms.onProtocolChange(events => wantedEvents.push(events));
6768
cms.activeProtocol = 'NONE';
@@ -76,7 +77,7 @@ describe('CoreMouseService', () => {
7677
let cms: CoreMouseService;
7778
let reports: string[];
7879
beforeEach(() => {
79-
cms = new CoreMouseService(bufferService, coreService);
80+
cms = new CoreMouseService(bufferService, coreService, optionsService);
8081
reports = [];
8182
coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data);
8283
coreService.triggerBinaryEvent = (data: string) => reports.push(data);

src/common/services/CoreMouseService.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
33
* @license MIT
44
*/
5-
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
5+
import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services';
66
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
77
import { Disposable } from 'vs/base/common/lifecycle';
88
import { Emitter } from 'vs/base/common/event';
@@ -174,13 +174,15 @@ export class CoreMouseService extends Disposable implements ICoreMouseService {
174174
private _activeProtocol: string = '';
175175
private _activeEncoding: string = '';
176176
private _lastEvent: ICoreMouseEvent | null = null;
177+
private _wheelPartialScroll: number = 0;
177178

178179
private readonly _onProtocolChange = this._register(new Emitter<CoreMouseEventType>());
179-
public readonly onProtocolChange = this._onProtocolChange.event;
180+
public readonly onProtocolChange = this._onProtocolChange.event;
180181

181182
constructor(
182183
@IBufferService private readonly _bufferService: IBufferService,
183-
@ICoreService private readonly _coreService: ICoreService
184+
@ICoreService private readonly _coreService: ICoreService,
185+
@IOptionsService private readonly _optionsService: IOptionsService
184186
) {
185187
super();
186188
// register default protocols and encodings
@@ -229,6 +231,49 @@ export class CoreMouseService extends Disposable implements ICoreMouseService {
229231
this.activeProtocol = 'NONE';
230232
this.activeEncoding = 'DEFAULT';
231233
this._lastEvent = null;
234+
this._wheelPartialScroll = 0;
235+
}
236+
237+
/**
238+
* Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls.
239+
* This prevents hyper-sensitive scrolling in alt buffer.
240+
*/
241+
public consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number {
242+
// Do nothing if it's not a vertical scroll event
243+
if (ev.deltaY === 0 || ev.shiftKey) {
244+
return 0;
245+
}
246+
247+
if (cellHeight === undefined || dpr === undefined) {
248+
return 0;
249+
}
250+
251+
const targetWheelEventPixels = cellHeight / dpr;
252+
let amount = this._applyScrollModifier(ev.deltaY, ev);
253+
254+
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
255+
amount /= (targetWheelEventPixels + 0.0); // Prevent integer division
256+
257+
const isLikelyTrackpad = Math.abs(ev.deltaY) < 50;
258+
if (isLikelyTrackpad) {
259+
amount *= 0.3;
260+
}
261+
262+
this._wheelPartialScroll += amount;
263+
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
264+
this._wheelPartialScroll %= 1;
265+
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
266+
amount *= this._bufferService.rows;
267+
}
268+
return amount;
269+
}
270+
271+
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
272+
// Multiply the scroll speed when the modifier key is pressed
273+
if (ev.altKey || ev.ctrlKey || ev.shiftKey) {
274+
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
275+
}
276+
return amount * this._optionsService.rawOptions.scrollSensitivity;
232277
}
233278

234279
/**

src/common/services/Services.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export interface ICoreMouseService {
5858
* Human readable version of mouse events.
5959
*/
6060
explainEvents(events: CoreMouseEventType): { [event: string]: boolean };
61+
62+
/**
63+
* Process wheel event taking partial scroll into account.
64+
*/
65+
consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number;
6166
}
6267

6368
export const ICoreService = createDecorator<ICoreService>('CoreService');

0 commit comments

Comments
 (0)