diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 6793266f6..828013e9e 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -78,7 +78,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { public browser: IBrowser = Browser as any; private _customKeyEventHandler: CustomKeyEventHandler | undefined; - private _customWheelEventHandler: CustomWheelEventHandler | undefined; // Browser services private readonly _decorationService: DecorationService; @@ -582,7 +581,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { )); this._instantiationService.setService(ISelectionService, this._selectionService); this._mouseService = this._instantiationService.createInstance(MouseService); - this._mouseService.setCustomWheelEventHandler(this._customWheelEventHandler); this._instantiationService.setService(IMouseService, this._mouseService); this._register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent))); this._register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); @@ -607,7 +605,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this._register(addDisposableListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e))); // apply mouse event classes set by escape codes before terminal was attached - if (this.coreMouseService.areMouseEventsActive) { + if (this.mouseStateService.areMouseEventsActive) { this._selectionService.disable(); this.element.classList.add('enable-mouse-events'); } else { @@ -727,8 +725,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { } public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void { - this._customWheelEventHandler = customWheelEventHandler; - this._mouseService?.setCustomWheelEventHandler(customWheelEventHandler); + this.mouseStateService.setCustomWheelEventHandler(customWheelEventHandler); } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { @@ -1098,6 +1095,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this._setup(); super.reset(); + this._mouseService?.reset(); this._selectionService?.reset(); this._decorationService.reset(); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index a682913e2..485900e41 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -12,7 +12,7 @@ import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, I import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal'; -import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services'; +import { IUnicodeService, IOptionsService, ICoreService, IMouseStateService } from 'common/services/Services'; import { IFunctionIdentifier, IParams } from 'common/parser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; @@ -51,7 +51,7 @@ export class MockTerminal implements ITerminal { public dimensions: IRenderDimensionsApi | undefined; public markers!: IMarker[]; public linkifier: ILinkifier2 | undefined; - public coreMouseService!: ICoreMouseService; + public mouseStateService!: IMouseStateService; public coreService!: ICoreService; public optionsService!: IOptionsService; public unicodeService!: IUnicodeService; @@ -393,13 +393,8 @@ export class MockMouseService implements IMouseService { throw new Error('Not implemented'); } - public setCustomWheelEventHandler(): void { - throw new Error('Not implemented'); - } - - public bindMouse(): void { - throw new Error('Not implemented'); - } + public bindMouse(): void { } + public reset(): void { } } export class MockRenderService implements IRenderService { diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 09864f7ac..c627c916e 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -6,7 +6,7 @@ import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { ViewportConstants } from 'browser/shared/Constants'; import { Disposable, toDisposable } from 'common/Lifecycle'; -import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IMouseStateService, IOptionsService } from 'common/services/Services'; import { CoreMouseEventType } from 'common/Types'; import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom'; import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement'; @@ -34,7 +34,7 @@ export class Viewport extends Disposable { screenElement: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @ICoreBrowserService coreBrowserService: ICoreBrowserService, - @ICoreMouseService coreMouseService: ICoreMouseService, + @IMouseStateService mouseStateService: IMouseStateService, @IThemeService themeService: IThemeService, @IOptionsService private readonly _optionsService: IOptionsService, @IRenderService private readonly _renderService: IRenderService @@ -65,7 +65,7 @@ export class Viewport extends Disposable { 'scrollbar' ], () => this._scrollableElement.updateOptions(this._getChangeOptions()))); // Don't handle mouse wheel if wheel events are supported by the current mouse prototcol - this._register(coreMouseService.onProtocolChange(type => { + this._register(mouseStateService.onProtocolChange(type => { this._scrollableElement.updateOptions({ handleMouseWheel: !(type & CoreMouseEventType.WHEEL) }); diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index f25da387e..75aada048 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -103,7 +103,7 @@ export class Terminal extends Disposable implements ITerminalApi { public get modes(): IModes { const m = this._core.coreService.decPrivateModes; let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none'; - switch (this._core.coreMouseService.activeProtocol) { + switch (this._core.mouseStateService.activeProtocol) { case 'X10': mouseTrackingMode = 'x10'; break; case 'VT200': mouseTrackingMode = 'vt200'; break; case 'DRAG': mouseTrackingMode = 'drag'; break; diff --git a/src/browser/services/MouseService.test.ts b/src/browser/services/MouseService.test.ts new file mode 100644 index 000000000..f395083e3 --- /dev/null +++ b/src/browser/services/MouseService.test.ts @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { MouseService } from 'browser/services/MouseService'; +import { MouseStateService } from 'common/services/MouseStateService'; +import { CoreMouseAction, CoreMouseButton } from 'common/Types'; +import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; +import { MockCoreBrowserService, MockRenderService, MockSelectionService } from 'browser/TestUtils.test'; + +function toBytes(s: string | undefined): number[] { + if (!s) { + return []; + } + const res: number[] = []; + for (let i = 0; i < s.length; ++i) { + res.push(s.charCodeAt(i)); + } + return res; +} + +// Minimal mocks for deps that MouseService touches in these tests +const bufferService: IBufferService = { + buffer: { hasScrollback: true } as any, + cols: 500, + rows: 500 +} as any; + +const optionsService: IOptionsService = { + rawOptions: { + logLevel: 'info', + fastScrollSensitivity: 1, + scrollSensitivity: 1 + } +} as any; + +const logService: ILogService = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} +} as any; + +describe('MouseService _triggerMouseEvent', () => { + let mouseService: MouseService; + let mouseStateService: MouseStateService; + let coreService: ICoreService; + let reports: string[]; + + beforeEach(() => { + reports = []; + mouseStateService = new MouseStateService(); + coreService = { + triggerDataEvent: (data: string) => reports.push(data), + triggerBinaryEvent: (data: string) => reports.push(data), + decPrivateModes: { applicationCursorKeys: false } + } as any; + + mouseService = new MouseService( + new MockRenderService(), + { + getMouseReportCoords: (_ev: MouseEvent, _el: HTMLElement) => ({ col: 0, row: 0, x: 0, y: 0 }) + } as any, + mouseStateService, + coreService, + bufferService, + optionsService, + new MockSelectionService(), + logService, + new MockCoreBrowserService() + ); + }); + + function trigger(e: Parameters[0]): boolean { + return (mouseService as any)._triggerMouseEvent(e); + } + + it('NONE', () => { + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); + }); + + it('X10', () => { + mouseStateService.activeProtocol = 'X10'; + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); + }); + + it('VT200', () => { + mouseStateService.activeProtocol = 'VT200'; + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); + }); + + it('DRAG', () => { + mouseStateService.activeProtocol = 'DRAG'; + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + }); + + it('ANY', () => { + mouseStateService.activeProtocol = 'ANY'; + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true); + // should not report in any case + // invalid button + action combinations + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false); + // invalid coords + assert.equal(trigger({ col: -1, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(trigger({ col: 500, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(trigger({ col: 0, row: -1, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + assert.equal(trigger({ col: 0, row: 500, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); + }); + + describe('coords', () => { + it('DEFAULT encoding', () => { + mouseStateService.activeProtocol = 'ANY'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + if (i > 222) { + // supress mouse reports if we are out of addressible range (max. 222) + assert.deepEqual(toBytes(reports.pop()), []); + } else { + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); + } + } + }); + + it('SGR encoding', () => { + mouseStateService.activeProtocol = 'ANY'; + mouseStateService.activeEncoding = 'SGR'; + for (let i = 0; i < bufferService.cols; ++i) { + assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`); + } + }); + + it('SGR_PIXELS encoding', () => { + mouseStateService.activeProtocol = 'ANY'; + mouseStateService.activeEncoding = 'SGR_PIXELS'; + for (let i = 0; i < 500; ++i) { + assert.equal(trigger({ col: 0, row: 0, x: i, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); + assert.deepEqual(reports.pop(), `\x1b[<0;${i};0M`); + } + }); + }); + + it('eventCodes with modifiers (DEFAULT encoding)', () => { + // TODO: implement AUX button tests + mouseStateService.activeProtocol = 'ANY'; + mouseStateService.activeEncoding = 'DEFAULT'; + // all buttons + down + no modifer + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']); + reports = []; + + // all buttons + up + no modifier + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']); + reports = []; + + // all buttons + move + no modifier + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); + assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']); + reports = []; + + // button none + move + modifiers + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true); + assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true); + assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']); + reports = []; + }); +}); diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index f53f22a57..ddb457365 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -4,11 +4,10 @@ */ import { addDisposableListener } from 'browser/Dom'; -import { IBufferService, ICoreMouseService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; -import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, IDisposable } from 'common/Types'; +import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from 'common/services/Services'; +import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; import { toDisposable } from 'common/Lifecycle'; -import { CustomWheelEventHandler } from 'browser/Types'; import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services'; type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>; @@ -22,12 +21,13 @@ interface IMouseBindContext { export class MouseService implements IMouseService { public serviceBrand: undefined; - private _customWheelEventHandler: CustomWheelEventHandler | undefined; + private _lastEvent: ICoreMouseEvent | null = null; + private _wheelPartialScroll: number = 0; constructor( @IRenderService private readonly _renderService: IRenderService, @IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService, - @ICoreMouseService private readonly _coreMouseService: ICoreMouseService, + @IMouseStateService private readonly _mouseStateService: IMouseStateService, @ICoreService private readonly _coreService: ICoreService, @IBufferService private readonly _bufferService: IBufferService, @IOptionsService private readonly _optionsService: IOptionsService, @@ -42,7 +42,7 @@ export class MouseService implements IMouseService { /** * Event listener state handling. - * We listen to the onProtocolChange event of CoreMouseService and put + * We listen to the onProtocolChange event of MouseStateService and put * requested listeners in `requestedEvents`. With this the listeners * have all bits to do the event listener juggling. * Note: 'mousedown' currently is "always on" and not managed @@ -61,11 +61,11 @@ export class MouseService implements IMouseService { mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent), mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent) }; - register(this._coreMouseService.onProtocolChange(events => { + register(this._mouseStateService.onProtocolChange(events => { this._handleProtocolChange(ctx, eventListeners, events); })); // force initial onProtocolChange so we dont miss early mouse requests - this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol; + this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol; // Ensure document-level listeners are removed on dispose register(toDisposable(() => { @@ -119,14 +119,14 @@ export class MouseService implements IMouseService { but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; break; case 'wheel': - if (this._customWheelEventHandler && this._customWheelEventHandler(ev as WheelEvent) === false) { + if (!this._mouseStateService.allowCustomWheelEvent(ev as WheelEvent)) { return false; } const deltaY = (ev as WheelEvent).deltaY; if (deltaY === 0) { return false; } - const lines = this._coreMouseService.consumeWheelEvent( + const lines = this._consumeWheelEvent( ev as WheelEvent, this._renderService?.dimensions?.device?.cell?.height, this._coreBrowserService?.dpr @@ -148,7 +148,7 @@ export class MouseService implements IMouseService { return false; } - return this._coreMouseService.triggerMouseEvent({ + return this._triggerMouseEvent({ col: pos.col, row: pos.row, x: pos.x, @@ -202,7 +202,7 @@ export class MouseService implements IMouseService { // Don't send the mouse button to the pty if mouse events are disabled or // if the selection manager is having selection forced (ie. a modifier is // held). - if (!this._coreMouseService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) { + if (!this._mouseStateService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) { return; } @@ -226,7 +226,7 @@ export class MouseService implements IMouseService { return; } - if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) { + if (!this._mouseStateService.allowCustomWheelEvent(ev)) { return false; } @@ -244,7 +244,7 @@ export class MouseService implements IMouseService { return false; } - const lines = this._coreMouseService.consumeWheelEvent( + const lines = this._consumeWheelEvent( ev, this._renderService?.dimensions?.device?.cell?.height, this._coreBrowserService?.dpr @@ -264,13 +264,18 @@ export class MouseService implements IMouseService { } } + public reset(): void { + this._lastEvent = null; + this._wheelPartialScroll = 0; + } + private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void { const { element, document } = ctx.target; const { requestedEvents } = ctx; // apply global changes on events if (events) { if (this._optionsService.rawOptions.logLevel === 'debug') { - this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events)); + this._logService.debug('Binding to mouse events:', this._explainEvents(events)); } element.classList.add('enable-mouse-events'); this._selectionService.disable(); @@ -320,7 +325,131 @@ export class MouseService implements IMouseService { } } - public setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void { - this._customWheelEventHandler = customWheelEventHandler; + 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; } + + /** + * Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls. + * This prevents hyper-sensitive scrolling in alt buffer. + */ + private _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; + } + + /** + * Triggers a mouse event to be sent. + * + * Returns true if the event passed all protocol restrictions and a report + * was sent, otherwise false. The return value may be used to decide whether + * the default event action in the browser component should be omitted. + * + * Note: The method will change values of the given event object + * to fulfill protocol and encoding restrictions. + */ + private _triggerMouseEvent(e: ICoreMouseEvent): boolean { + // range check for col/row + if (e.col < 0 || e.col >= this._bufferService.cols + || e.row < 0 || e.row >= this._bufferService.rows) { + return false; + } + + // filter nonsense combinations of button + action + if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) { + return false; + } + if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) { + return false; + } + if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) { + return false; + } + + // report 1-based coords + e.col++; + e.row++; + + // debounce move events at grid or pixel level + if (e.action === CoreMouseAction.MOVE + && this._lastEvent + && this._equalEvents(this._lastEvent, e, this._mouseStateService.isPixelEncoding) + ) { + return false; + } + + // apply protocol restrictions + if (!this._mouseStateService.restrictMouseEvent(e)) { + return false; + } + + // encode report and send + const report = this._mouseStateService.encodeMouseEvent(e); + if (report) { + if (this._mouseStateService.isDefaultEncoding) { + this._coreService.triggerBinaryEvent(report); + } else { + this._coreService.triggerDataEvent(report, true); + } + } + + this._lastEvent = e; + return true; + } + + private _explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { + return { + down: !!(events & CoreMouseEventType.DOWN), + up: !!(events & CoreMouseEventType.UP), + drag: !!(events & CoreMouseEventType.DRAG), + move: !!(events & CoreMouseEventType.MOVE), + wheel: !!(events & CoreMouseEventType.WHEEL) + }; + } + + private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean { + if (pixels) { + if (e1.x !== e2.x) return false; + if (e1.y !== e2.y) return false; + } else { + if (e1.col !== e2.col) return false; + if (e1.row !== e2.row) return false; + } + if (e1.button !== e2.button) return false; + if (e1.action !== e2.action) return false; + if (e1.ctrl !== e2.ctrl) return false; + if (e1.alt !== e2.alt) return false; + if (e1.shift !== e2.shift) return false; + return true; + } + } diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 7f1c10576..47c46ec66 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -4,7 +4,7 @@ */ import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; -import { CustomWheelEventHandler, IColorSet, ILink, ReadonlyColorSet } from 'browser/Types'; +import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { AllColorIndex, IDisposable, IKeyboardResult } from 'common/Types'; @@ -61,8 +61,8 @@ export const IMouseService = createDecorator('MouseService'); export interface IMouseService { serviceBrand: undefined; - setCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler | undefined): void; bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void; + reset(): void; } export interface IMouseServiceTarget { element: HTMLElement; diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 0ce19a4d1..e2a3adf72 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -21,14 +21,14 @@ * http://linux.die.net/man/7/urxvt */ -import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; +import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; import { InstantiationService } from 'common/services/InstantiationService'; import { LogService } from 'common/services/LogService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { OptionsService } from 'common/services/OptionsService'; import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent } from 'common/Types'; import { CoreService } from 'common/services/CoreService'; -import { CoreMouseService } from 'common/services/CoreMouseService'; +import { MouseStateService } from 'common/services/MouseStateService'; import { UnicodeService } from 'common/services/UnicodeService'; import { CharsetService } from 'common/services/CharsetService'; import { updateWindowsModeWrappedState } from 'common/WindowsMode'; @@ -50,7 +50,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected readonly _charsetService: ICharsetService; protected readonly _oscLinkService: IOscLinkService; - public readonly coreMouseService: ICoreMouseService; + public readonly mouseStateService: IMouseStateService; public readonly coreService: ICoreService; public readonly unicodeService: IUnicodeService; public readonly optionsService: IOptionsService; @@ -113,8 +113,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(IBufferService, this._bufferService); this.coreService = this._register(this._instantiationService.createInstance(CoreService)); this._instantiationService.setService(ICoreService, this.coreService); - this.coreMouseService = this._register(this._instantiationService.createInstance(CoreMouseService)); - this._instantiationService.setService(ICoreMouseService, this.coreMouseService); + this.mouseStateService = this._register(this._instantiationService.createInstance(MouseStateService)); + this._instantiationService.setService(IMouseStateService, this.mouseStateService); this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService)); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); @@ -124,7 +124,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { // Register input handler and handle/forward events - this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService)); + this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.mouseStateService, this.unicodeService)); this._register(EventUtils.forward(this._inputHandler.onLineFeed, this._onLineFeed)); // Setup listeners @@ -256,7 +256,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._bufferService.reset(); this._charsetService.reset(); this.coreService.reset(); - this.coreMouseService.reset(); + this.mouseStateService.reset(); } diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index f36c28b45..4d19ef4f1 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes, BgFlags, UnderlineStyle } from 'common/buffer/Constants'; import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockMouseStateService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; import { IBufferService, ICoreService, type IOscLinkService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -70,7 +70,7 @@ describe('InputHandler', () => { coreService = new CoreService(bufferService, new MockLogService(), optionsService); oscLinkService = new OscLinkService(bufferService); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockMouseStateService(), new MockUnicodeService()); }); describe('SL/SR/DECIC/DECDC', () => { @@ -261,7 +261,7 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle bracketedPasteMode', () => { const coreService = new MockCoreService(); - const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, true); @@ -272,7 +272,7 @@ describe('InputHandler', () => { it('should toggle colorSchemeUpdates (DECSET 2031)', () => { const coreService = new MockCoreService(); const optionsService = new MockOptionsService(); - const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService()); // Set color scheme updates mode (default colorSchemeQuery=true) inputHandler.setModePrivate(Params.fromArray([2031])); assert.equal(coreService.decPrivateModes.colorSchemeUpdates, true); @@ -284,7 +284,7 @@ describe('InputHandler', () => { const coreService = new MockCoreService(); const optionsService = new MockOptionsService(); optionsService.rawOptions.vtExtensions = { colorSchemeQuery: false }; - const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService()); // Attempt to set color scheme updates mode inputHandler.setModePrivate(Params.fromArray([2031])); assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false); @@ -306,7 +306,7 @@ describe('InputHandler', () => { new MockLogService(), new MockOptionsService(), new MockOscLinkService(), - new MockCoreMouseService(), + new MockMouseStateService(), new MockUnicodeService() ); @@ -352,7 +352,7 @@ describe('InputHandler', () => { new MockLogService(), new MockOptionsService(), new MockOscLinkService(), - new MockCoreMouseService(), + new MockMouseStateService(), new MockUnicodeService() ); @@ -402,7 +402,7 @@ describe('InputHandler', () => { new MockLogService(), new MockOptionsService(), new MockOscLinkService(), - new MockCoreMouseService(), + new MockMouseStateService(), new MockUnicodeService() ); @@ -439,7 +439,7 @@ describe('InputHandler', () => { new MockLogService(), new MockOptionsService(), new MockOscLinkService(), - new MockCoreMouseService(), + new MockMouseStateService(), new MockUnicodeService() ); @@ -488,7 +488,7 @@ describe('InputHandler', () => { new MockLogService(), new MockOptionsService({ scrollOnEraseInDisplay: true }), new MockOscLinkService(), - new MockCoreMouseService(), + new MockMouseStateService(), new MockUnicodeService() ); const aLine = Array(bufferService.cols + 1).join('a'); @@ -520,7 +520,7 @@ describe('InputHandler', () => { new MockLogService(), new MockOptionsService(), new MockOscLinkService(), - new MockCoreMouseService(), + new MockMouseStateService(), new MockUnicodeService() ); @@ -646,7 +646,7 @@ describe('InputHandler', () => { new MockLogService(), new MockOptionsService(), new MockOscLinkService(), - new MockCoreMouseService(), + new MockMouseStateService(), new MockUnicodeService() ); const container = new Uint32Array(10); @@ -674,7 +674,7 @@ describe('InputHandler', () => { beforeEach(() => { bufferService = new MockBufferService(80, 30); - handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => { await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); @@ -887,7 +887,7 @@ describe('InputHandler', () => { describe('colon notation', () => { let inputHandler2: TestInputHandler; beforeEach(() => { - inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService()); }); describe('should equal to semicolon', () => { it('CSI 38:2::50:100:150 m', async () => { @@ -2461,7 +2461,7 @@ describe('InputHandler', () => { bufferService = new BufferService(optionsService, new MockLogService()); bufferService.resize(80, 30); coreService = new CoreService(bufferService, new MockLogService(), optionsService); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService()); }); describe('stack limit', () => { @@ -2513,7 +2513,7 @@ describe('InputHandler', () => { coreService = new CoreService(bufferService, new MockLogService(), optionsService); coreService.onData(data => { console.log(data); }); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService()); }); it('async CUP with CPR check', async () => { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 327eb2ec6..68265c5df 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -15,7 +15,7 @@ import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } fr import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; -import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IMouseStateService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; import { UnicodeService } from 'common/services/UnicodeService'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; @@ -178,7 +178,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _logService: ILogService, private readonly _optionsService: IOptionsService, private readonly _oscLinkService: IOscLinkService, - private readonly _coreMouseService: ICoreMouseService, + private readonly _mouseStateService: IMouseStateService, private readonly _unicodeService: IUnicodeService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() ) { @@ -1987,19 +1987,19 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. - this._coreMouseService.activeProtocol = 'X10'; + this._mouseStateService.activeProtocol = 'X10'; break; case 1000: // vt200 mouse // no motion. - this._coreMouseService.activeProtocol = 'VT200'; + this._mouseStateService.activeProtocol = 'VT200'; break; case 1002: // button event mouse - this._coreMouseService.activeProtocol = 'DRAG'; + this._mouseStateService.activeProtocol = 'DRAG'; break; case 1003: // any event mouse // any event - sends motion events, // even if there is no button held down. - this._coreMouseService.activeProtocol = 'ANY'; + this._mouseStateService.activeProtocol = 'ANY'; break; case 1004: // send focusin/focusout events // focusin: ^[[I @@ -2011,13 +2011,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._logService.debug('DECSET 1005 not supported (see #2507)'); break; case 1006: // sgr ext mode mouse - this._coreMouseService.activeEncoding = 'SGR'; + this._mouseStateService.activeEncoding = 'SGR'; break; case 1015: // urxvt ext mode mouse - removed in #2507 this._logService.debug('DECSET 1015 not supported (see #2507)'); break; case 1016: // sgr pixels mode mouse - this._coreMouseService.activeEncoding = 'SGR_PIXELS'; + this._mouseStateService.activeEncoding = 'SGR_PIXELS'; break; case 25: // show cursor this._coreService.isCursorHidden = false; @@ -2248,7 +2248,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 1000: // vt200 mouse case 1002: // button event mouse case 1003: // any event mouse - this._coreMouseService.activeProtocol = 'NONE'; + this._mouseStateService.activeProtocol = 'NONE'; break; case 1004: // send focusin/focusout events this._coreService.decPrivateModes.sendFocus = false; @@ -2257,13 +2257,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._logService.debug('DECRST 1005 not supported (see #2507)'); break; case 1006: // sgr ext mode mouse - this._coreMouseService.activeEncoding = 'DEFAULT'; + this._mouseStateService.activeEncoding = 'DEFAULT'; break; case 1015: // urxvt ext mode mouse - removed in #2507 this._logService.debug('DECRST 1015 not supported (see #2507)'); break; case 1016: // sgr pixels mode mouse - this._coreMouseService.activeEncoding = 'DEFAULT'; + this._mouseStateService.activeEncoding = 'DEFAULT'; break; case 25: // hide cursor this._coreService.isCursorHidden = true; @@ -2357,7 +2357,7 @@ export class InputHandler extends Disposable implements IInputHandler { // access helpers const dm = this._coreService.decPrivateModes; - const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._coreMouseService; + const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._mouseStateService; const cs = this._coreService; const { buffers, cols } = this._bufferService; const { active, alt } = buffers; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 0f982b302..0884a4ab4 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, UnicodeCharProperties, UnicodeCharWidth, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService, type IBufferResizeEvent } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IMouseStateService, ICharsetService, UnicodeCharProperties, UnicodeCharWidth, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService, type IBufferResizeEvent } from 'common/services/Services'; import { UnicodeService } from 'common/services/UnicodeService'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -66,22 +66,21 @@ export class MockBufferService implements IBufferService { public reset(): void { } } -export class MockCoreMouseService implements ICoreMouseService { +export class MockMouseStateService implements IMouseStateService { public serviceBrand: any; public areMouseEventsActive: boolean = false; public activeEncoding: string = ''; public activeProtocol: string = ''; + public isDefaultEncoding: boolean = true; + public isPixelEncoding: boolean = false; public addEncoding(name: string): void { } public addProtocol(name: string): void { } public reset(): void { } - public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } public onProtocolChange: IEvent = new Emitter().event; - public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { - throw new Error('Method not implemented.'); - } - public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number { - return 1; - } + public restrictMouseEvent(event: ICoreMouseEvent): boolean { return true; } + public encodeMouseEvent(event: ICoreMouseEvent): string { return ''; } + public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void { } + public allowCustomWheelEvent(ev: WheelEvent): boolean { return true; } } export class MockCharsetService implements ICharsetService { diff --git a/src/common/Types.ts b/src/common/Types.ts index 7ceda21d2..0a9c89a1a 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -7,12 +7,12 @@ import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; import { UnderlineStyle } from 'common/buffer/Constants'; import { IBufferSet } from 'common/buffer/Types'; import { IParams } from 'common/parser/Types'; -import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; +import { IMouseStateService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from '@xterm/xterm'; import type { Emitter, IEvent } from 'common/Event'; export interface ICoreTerminal { - coreMouseService: ICoreMouseService; + mouseStateService: IMouseStateService; coreService: ICoreService; optionsService: IOptionsService; unicodeService: IUnicodeService; @@ -343,7 +343,7 @@ export interface ICoreMouseEvent { * it is not possible to report multiple buttons at once. * Wheel is treated as a button. * There are invalid combinations of buttons and actions possible - * (like move + wheel), those are silently ignored by the CoreMouseService. + * (like move + wheel), those are silently ignored by the MouseStateService. */ button: CoreMouseButton; action: CoreMouseAction; @@ -360,7 +360,7 @@ export interface ICoreMouseEvent { * CoreMouseEventType * To be reported to the browser component which events a mouse * protocol wants to be catched and forwarded as an ICoreMouseEvent - * to CoreMouseService. + * to MouseStateService. */ export const enum CoreMouseEventType { NONE = 0, @@ -378,7 +378,7 @@ export const enum CoreMouseEventType { /** * Mouse protocol interface. - * A mouse protocol can be registered and activated at the CoreMouseService. + * A mouse protocol can be registered and activated at the MouseStateService. * `events` should contain a list of needed events as a hint for the browser component * to install/remove the appropriate event handlers. * `restrict` applies further protocol specific restrictions like not allowed @@ -391,7 +391,7 @@ export interface ICoreMouseProtocol { /** * CoreMouseEncoding - * The tracking encoding can be registered and activated at the CoreMouseService. + * The tracking encoding can be registered and activated at the MouseStateService. * If a ICoreMouseEvent passes all procotol restrictions it will be encoded * with the active encoding and sent out. * Note: Returning an empty string will supress sending a mouse report, diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts deleted file mode 100644 index 3b79596d2..000000000 --- a/src/common/services/CoreMouseService.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ -import { CoreMouseService } from 'common/services/CoreMouseService'; -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) { - return []; - } - const res: number[] = []; - for (let i = 0; i < s.length; ++i) { - res.push(s.charCodeAt(i)); - } - return res; -} - -describe('CoreMouseService', () => { - it('init', () => { - 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, 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, optionsService); - assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']); - }); - it('protocol/encoding setter, reset', () => { - const cms = new CoreMouseService(bufferService, coreService, optionsService); - cms.activeEncoding = 'SGR'; - cms.activeProtocol = 'ANY'; - assert.equal(cms.activeEncoding, 'SGR'); - assert.equal(cms.activeProtocol, 'ANY'); - cms.reset(); - assert.equal(cms.activeEncoding, 'DEFAULT'); - assert.equal(cms.activeProtocol, 'NONE'); - assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"'); - assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"'); - }); - it('addEncoding', () => { - 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, 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, optionsService); - const wantedEvents: CoreMouseEventType[] = []; - cms.onProtocolChange(events => wantedEvents.push(events)); - cms.activeProtocol = 'NONE'; - assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]); - cms.activeProtocol = 'ANY'; - assert.deepEqual(wantedEvents, [ - CoreMouseEventType.NONE, - CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE - ]); - }); - describe('triggerMouseEvent', () => { - let cms: CoreMouseService; - let reports: string[]; - beforeEach(() => { - cms = new CoreMouseService(bufferService, coreService, optionsService); - reports = []; - coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); - coreService.triggerBinaryEvent = (data: string) => reports.push(data); - }); - it('NONE', () => { - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); - }); - it('X10', () => { - cms.activeProtocol = 'X10'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); - }); - it('VT200', () => { - cms.activeProtocol = 'VT200'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false); - }); - it('DRAG', () => { - cms.activeProtocol = 'DRAG'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); - }); - it('ANY', () => { - cms.activeProtocol = 'ANY'; - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true); - // should not report in any case - // invalid button + action combinations - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false); - // invalid coords - assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); - assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); - }); - describe('coords', () => { - it('DEFAULT encoding', () => { - cms.activeProtocol = 'ANY'; - for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - if (i > 222) { - // supress mouse reports if we are out of addressible range (max. 222) - assert.deepEqual(toBytes(reports.pop()), []); - } else { - assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); - } - } - }); - it('SGR encoding', () => { - cms.activeProtocol = 'ANY'; - cms.activeEncoding = 'SGR'; - for (let i = 0; i < bufferService.cols; ++i) { - assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`); - } - }); - it('SGR_PIXELS encoding', () => { - cms.activeProtocol = 'ANY'; - cms.activeEncoding = 'SGR_PIXELS'; - for (let i = 0; i < 500; ++i) { - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: i, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - assert.deepEqual(reports.pop(), `\x1b[<0;${i};0M`); - } - }); - }); - it('eventCodes with modifiers (DEFAULT encoding)', () => { - // TODO: implement AUX button tests - cms.activeProtocol = 'ANY'; - cms.activeEncoding = 'DEFAULT'; - // all buttons + down + no modifer - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true); - assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']); - reports = []; - - // all buttons + up + no modifier - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true); - assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']); - reports = []; - - // all buttons + move + no modifier - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true); - assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']); - reports = []; - - // button none + move + modifiers - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true); - assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true); - assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']); - reports = []; - }); - }); -}); diff --git a/src/common/services/MouseStateService.test.ts b/src/common/services/MouseStateService.test.ts new file mode 100644 index 000000000..0f2cd489a --- /dev/null +++ b/src/common/services/MouseStateService.test.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { MouseStateService } from 'common/services/MouseStateService'; +import { assert } from 'chai'; +import { ICoreMouseEvent, CoreMouseEventType } from 'common/Types'; + +function toBytes(s: string | undefined): number[] { + if (!s) { + return []; + } + const res: number[] = []; + for (let i = 0; i < s.length; ++i) { + res.push(s.charCodeAt(i)); + } + return res; +} + +describe('MouseStateService', () => { + it('init', () => { + const cms = new MouseStateService(); + assert.equal(cms.activeEncoding, 'DEFAULT'); + assert.equal(cms.activeProtocol, 'NONE'); + }); + it('default protocols - NONE, X10, VT200, DRAG, ANY', () => { + const cms = new MouseStateService(); + assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']); + }); + it('default encodings - DEFAULT, SGR', () => { + const cms = new MouseStateService(); + assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']); + }); + it('protocol/encoding setter, reset', () => { + const cms = new MouseStateService(); + cms.activeEncoding = 'SGR'; + cms.activeProtocol = 'ANY'; + assert.equal(cms.activeEncoding, 'SGR'); + assert.equal(cms.activeProtocol, 'ANY'); + cms.reset(); + assert.equal(cms.activeEncoding, 'DEFAULT'); + assert.equal(cms.activeProtocol, 'NONE'); + assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"'); + assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"'); + }); + it('addEncoding', () => { + const cms = new MouseStateService(); + cms.addEncoding('XYZ', (e: ICoreMouseEvent) => ''); + cms.activeEncoding = 'XYZ'; + assert.equal(cms.activeEncoding, 'XYZ'); + }); + it('addProtocol', () => { + const cms = new MouseStateService(); + cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false }); + cms.activeProtocol = 'XYZ'; + assert.equal(cms.activeProtocol, 'XYZ'); + }); + it('onProtocolChange', () => { + const cms = new MouseStateService(); + const wantedEvents: CoreMouseEventType[] = []; + cms.onProtocolChange(events => wantedEvents.push(events)); + cms.activeProtocol = 'NONE'; + assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]); + cms.activeProtocol = 'ANY'; + assert.deepEqual(wantedEvents, [ + CoreMouseEventType.NONE, + CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE + ]); + }); + it('restrictMouseEvent/encodeMouseEvent', () => { + const cms = new MouseStateService(); + const event: ICoreMouseEvent = { + col: 1, + row: 1, + x: 0, + y: 0, + button: 0, + action: 1, + ctrl: false, + alt: false, + shift: false + }; + cms.activeProtocol = 'ANY'; + cms.activeEncoding = 'DEFAULT'; + assert.equal(cms.restrictMouseEvent(event), true); + assert.deepEqual(toBytes(cms.encodeMouseEvent(event)), [0x1b, 0x5b, 0x4d, 0x20, 0x21, 0x21]); + }); +}); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/MouseStateService.ts similarity index 57% rename from src/common/services/CoreMouseService.ts rename to src/common/services/MouseStateService.ts index d569f0460..21420460a 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/MouseStateService.ts @@ -2,7 +2,7 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ -import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services'; +import { IMouseStateService } from 'common/services/Services'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { Disposable } from 'common/Lifecycle'; import { Emitter } from 'common/Event'; @@ -151,7 +151,7 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = { }; /** - * CoreMouseService + * MouseStateService * * Provides mouse tracking reports with different protocols and encodings. * - protocols: NONE (default), X10, VT200, DRAG, ANY @@ -166,25 +166,21 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = { * a tracking report to the backend based on protocol and encoding limitations. * To send a mouse event call `triggerMouseEvent`. */ -export class CoreMouseService extends Disposable implements ICoreMouseService { +export class MouseStateService extends Disposable implements IMouseStateService { public serviceBrand: any; private _protocols: { [name: string]: ICoreMouseProtocol } = {}; private _encodings: { [name: string]: CoreMouseEncoding } = {}; private _activeProtocol: string = ''; private _activeEncoding: string = ''; - private _lastEvent: ICoreMouseEvent | null = null; - private _wheelPartialScroll: number = 0; + private _customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined; private readonly _onProtocolChange = this._register(new Emitter()); public readonly onProtocolChange = this._onProtocolChange.event; - constructor( - @IBufferService private readonly _bufferService: IBufferService, - @ICoreService private readonly _coreService: ICoreService, - @IOptionsService private readonly _optionsService: IOptionsService - ) { + constructor() { super(); + // register default protocols and encodings for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]); for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]); @@ -230,136 +226,29 @@ export class CoreMouseService extends Disposable implements ICoreMouseService { public reset(): void { 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; + public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void { + this._customWheelEventHandler = customWheelEventHandler; } - 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; + public allowCustomWheelEvent(ev: WheelEvent): boolean { + return this._customWheelEventHandler ? this._customWheelEventHandler(ev) !== false : true; } - /** - * Triggers a mouse event to be sent. - * - * Returns true if the event passed all protocol restrictions and a report - * was sent, otherwise false. The return value may be used to decide whether - * the default event action in the bowser component should be omitted. - * - * Note: The method will change values of the given event object - * to fullfill protocol and encoding restrictions. - */ - public triggerMouseEvent(e: ICoreMouseEvent): boolean { - // range check for col/row - if (e.col < 0 || e.col >= this._bufferService.cols - || e.row < 0 || e.row >= this._bufferService.rows) { - return false; - } - - // filter nonsense combinations of button + action - if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) { - return false; - } - if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) { - return false; - } - if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) { - return false; - } - - // report 1-based coords - e.col++; - e.row++; - - // debounce move events at grid or pixel level - if (e.action === CoreMouseAction.MOVE - && this._lastEvent - && this._equalEvents(this._lastEvent, e, this._activeEncoding === 'SGR_PIXELS') - ) { - return false; - } - - // apply protocol restrictions - if (!this._protocols[this._activeProtocol].restrict(e)) { - return false; - } - - // encode report and send - const report = this._encodings[this._activeEncoding](e); - if (report) { - // always send DEFAULT as binary data - if (this._activeEncoding === 'DEFAULT') { - this._coreService.triggerBinaryEvent(report); - } else { - this._coreService.triggerDataEvent(report, true); - } - } - - this._lastEvent = e; - - return true; + public restrictMouseEvent(e: ICoreMouseEvent): boolean { + return this._protocols[this._activeProtocol].restrict(e); } - public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { - return { - down: !!(events & CoreMouseEventType.DOWN), - up: !!(events & CoreMouseEventType.UP), - drag: !!(events & CoreMouseEventType.DRAG), - move: !!(events & CoreMouseEventType.MOVE), - wheel: !!(events & CoreMouseEventType.WHEEL) - }; + public encodeMouseEvent(e: ICoreMouseEvent): string { + return this._encodings[this._activeEncoding](e); } - private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean { - if (pixels) { - if (e1.x !== e2.x) return false; - if (e1.y !== e2.y) return false; - } else { - if (e1.col !== e2.col) return false; - if (e1.row !== e2.row) return false; - } - if (e1.button !== e2.button) return false; - if (e1.action !== e2.action) return false; - if (e1.ctrl !== e2.ctrl) return false; - if (e1.alt !== e2.alt) return false; - if (e1.shift !== e2.shift) return false; - return true; + public get isDefaultEncoding(): boolean { + return this._activeEncoding === 'DEFAULT'; + } + + public get isPixelEncoding(): boolean { + return this._activeEncoding === 'SGR_PIXELS'; } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e0af27eee..a15dc23dc 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -33,8 +33,8 @@ export interface IBufferResizeEvent { rowsChanged: boolean; } -export const ICoreMouseService = createDecorator('CoreMouseService'); -export interface ICoreMouseService { +export const IMouseStateService = createDecorator('MouseStateService'); +export interface IMouseStateService { serviceBrand: undefined; activeProtocol: string; @@ -43,33 +43,17 @@ export interface ICoreMouseService { addProtocol(name: string, protocol: ICoreMouseProtocol): void; addEncoding(name: string, encoding: CoreMouseEncoding): void; reset(): void; - - /** - * Triggers a mouse event to be sent. - * - * Returns true if the event passed all protocol restrictions and a report - * was sent, otherwise false. The return value may be used to decide whether - * the default event action in the bowser component should be omitted. - * - * Note: The method will change values of the given event object - * to fullfill protocol and encoding restrictions. - */ - triggerMouseEvent(event: ICoreMouseEvent): boolean; + setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void; + allowCustomWheelEvent(ev: WheelEvent): boolean; /** * Event to announce changes in mouse tracking. */ onProtocolChange: IEvent; - - /** - * 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; + restrictMouseEvent(event: ICoreMouseEvent): boolean; + encodeMouseEvent(event: ICoreMouseEvent): string; + readonly isDefaultEncoding: boolean; + readonly isPixelEncoding: boolean; } export const ICoreService = createDecorator('CoreService'); diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 726149338..0d4e6e93b 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -103,7 +103,7 @@ export class Terminal extends Disposable implements ITerminalApi { public get modes(): IModes { const m = this._core.coreService.decPrivateModes; let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none'; - switch (this._core.coreMouseService.activeProtocol) { + switch (this._core.mouseStateService.activeProtocol) { case 'X10': mouseTrackingMode = 'x10'; break; case 'VT200': mouseTrackingMode = 'vt200'; break; case 'DRAG': mouseTrackingMode = 'drag'; break; diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index a62d1fbcc..eec2938a8 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -584,13 +584,13 @@ test.describe('InputHandler Integration Tests', () => { }); test('Ps = 1 0 0 5 - Enable UTF-8 Mouse Mode, xterm', async () => { await ctx.proxy.write('\x1b[?1006h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); await ctx.proxy.write('\x1b[?1005h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); }); test('Ps = 1 0 0 6 - Enable SGR Mouse Mode, xterm', async () => { await ctx.proxy.write('\x1b[?1006h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); }); test('Ps = 1 0 0 7 - Enable Alternate Scroll Mode, xterm', async () => { await assertNoModeChange('\x1b[?1007h'); @@ -603,13 +603,13 @@ test.describe('InputHandler Integration Tests', () => { }); test('Ps = 1 0 1 5 - Enable urxvt Mouse Mode', async () => { await ctx.proxy.write('\x1b[?1006h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); await ctx.proxy.write('\x1b[?1015h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); }); test('Ps = 1 0 1 6 - Enable SGR Mouse PixelMode, xterm', async () => { await ctx.proxy.write('\x1b[?1016h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR_PIXELS'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR_PIXELS'); }); test('Ps = 1 0 3 4 - Interpret "meta" key, xterm', async () => { await assertNoModeChange('\x1b[?1034h'); @@ -916,15 +916,15 @@ test.describe('InputHandler Integration Tests', () => { }); test('Ps = 1 0 0 5 - Disable UTF-8 Mouse Mode, xterm.', async () => { await ctx.proxy.write('\x1b[?1006h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); await ctx.proxy.write('\x1b[?1005l'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); }); test('Ps = 1 0 0 6 - Disable SGR Mouse Mode, xterm.', async () => { await ctx.proxy.write('\x1b[?1006h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); await ctx.proxy.write('\x1b[?1006l'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'DEFAULT'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'DEFAULT'); }); test('Ps = 1 0 0 7 - Disable Alternate Scroll Mode, xterm. This corresponds to the alternateScroll resource.', async () => { await assertNoModeChange('\x1b[?1007l'); @@ -937,15 +937,15 @@ test.describe('InputHandler Integration Tests', () => { }); test('Ps = 1 0 1 5 - Disable urxvt Mouse Mode.', async () => { await ctx.proxy.write('\x1b[?1006h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); await ctx.proxy.write('\x1b[?1015l'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR'); }); test('Ps = 1 0 1 6 - Disable SGR Mouse Pixel-Mode, xterm.', async () => { await ctx.proxy.write('\x1b[?1016h'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'SGR_PIXELS'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'SGR_PIXELS'); await ctx.proxy.write('\x1b[?1016l'); - await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding), 'DEFAULT'); + await pollFor(ctx.page, async () => await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding), 'DEFAULT'); }); test('Ps = 1 0 3 4 - Don\'t interpret "meta" key, xterm. This disables the eightBitInput resource.', async () => { await assertNoModeChange('\x1b[?1034l'); @@ -1849,8 +1849,8 @@ async function getModeSnapshot(): Promise { cols: await ctx.proxy.cols, rows: await ctx.proxy.rows, bufferType: await ctx.proxy.buffer.active.type, - mouseProtocol: await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeProtocol), - mouseEncoding: await ctx.proxy.core.evaluate(([core]) => core.coreMouseService.activeEncoding) + mouseProtocol: await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeProtocol), + mouseEncoding: await ctx.proxy.core.evaluate(([core]) => core.mouseStateService.activeEncoding) }; }