Merge remote-tracking branch 'upstream/master' into 5377_scroll
This commit is contained in:
@@ -9,7 +9,7 @@ import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockBufferService } from 'common/TestUtils.test';
|
||||
import { MockBufferService, createCellData } from 'common/TestUtils.test';
|
||||
|
||||
describe('CharacterJoinerService', () => {
|
||||
let service: ICharacterJoinerService;
|
||||
@@ -25,17 +25,17 @@ describe('CharacterJoinerService', () => {
|
||||
lines.set(4, new BufferLine(0));
|
||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||
const line6 = lineData([['wi']]);
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '¥', 2, '¥'.charCodeAt(0)]));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '', 0, 0]));
|
||||
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
|
||||
line6.resize(line6.length + 1, createCellData(0, '', 0));
|
||||
let sub = lineData([['deemo']]);
|
||||
let oldSize = line6.length;
|
||||
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
|
||||
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
|
||||
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513]));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)]));
|
||||
line6.resize(line6.length + 1, createCellData(0, ' ', 1));
|
||||
sub = lineData([['jiabc']]);
|
||||
oldSize = line6.length;
|
||||
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
|
||||
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
|
||||
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
|
||||
lines.set(6, line6);
|
||||
|
||||
@@ -272,8 +272,8 @@ function lineData(data: IPartialLineData[]): IBufferLine {
|
||||
const line = data[i][0];
|
||||
const attr = (data[i][1] || 0) as number;
|
||||
const offset = tline.length;
|
||||
tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0]));
|
||||
line.split('').map((char, idx) => tline.setCell(idx + offset, CellData.fromCharData([attr, char, 1, char.charCodeAt(0)])));
|
||||
tline.resize(tline.length + line.split('').length, createCellData(0, '', 0));
|
||||
line.split('').map((char, idx) => tline.setCell(idx + offset, createCellData(attr, char, 1)));
|
||||
}
|
||||
return tline;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
|
||||
|
||||
private _isFocused = false;
|
||||
private _cachedIsFocused: boolean | undefined = undefined;
|
||||
private _screenDprMonitor = this._register(new ScreenDprMonitor(this._window));
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private readonly _onDprChange = this._register(new Emitter<number>());
|
||||
public readonly onDprChange = this._onDprChange.event;
|
||||
@@ -27,6 +27,8 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
|
||||
) {
|
||||
super();
|
||||
|
||||
this._screenDprMonitor = this._register(new ScreenDprMonitor(this._window));
|
||||
|
||||
// Monitor device pixel ratio
|
||||
this._register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
|
||||
this._register(EventUtils.forward(this._screenDprMonitor.onDprChange, this._onDprChange));
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { getWindow } from 'browser/Dom';
|
||||
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { ICharSizeService, IMouseCoordsService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
export class MouseCoordsService implements IMouseCoordsService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
}
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
rowCount,
|
||||
this._charSizeService.hasValidSize,
|
||||
this._renderService.dimensions.css.cell.width,
|
||||
this._renderService.dimensions.css.cell.height,
|
||||
isSelection
|
||||
);
|
||||
}
|
||||
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return undefined;
|
||||
}
|
||||
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
|
||||
return {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<any>[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 = [];
|
||||
});
|
||||
});
|
||||
@@ -3,45 +3,537 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { getWindow } from 'browser/Dom';
|
||||
import { ICharSizeService, IRenderService, IMouseService } from './Services';
|
||||
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
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 { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
|
||||
import { Gesture, EventType as GestureEventType, IGestureEvent } from 'browser/scrollable/touch';
|
||||
|
||||
type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>;
|
||||
|
||||
interface IMouseBindContext {
|
||||
readonly target: IMouseServiceTarget;
|
||||
readonly focus: () => void;
|
||||
readonly requestedEvents: RequestedMouseEvents;
|
||||
}
|
||||
|
||||
export class MouseService implements IMouseService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _lastEvent: ICoreMouseEvent | null = null;
|
||||
private _wheelPartialScroll: number = 0;
|
||||
private _touchScrollAccumulator: number = 0;
|
||||
|
||||
constructor(
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@IMouseStateService private readonly _mouseStateService: IMouseStateService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ISelectionService private readonly _selectionService: ISelectionService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
}
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
rowCount,
|
||||
this._charSizeService.hasValidSize,
|
||||
this._renderService.dimensions.css.cell.width,
|
||||
this._renderService.dimensions.css.cell.height,
|
||||
isSelection
|
||||
);
|
||||
public bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void {
|
||||
const { element, document } = target;
|
||||
|
||||
/**
|
||||
* Event listener state handling.
|
||||
* 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
|
||||
* by onProtocolChange.
|
||||
*/
|
||||
const requestedEvents: RequestedMouseEvents = {
|
||||
mouseup: null,
|
||||
wheel: null,
|
||||
mousedrag: null,
|
||||
mousemove: null
|
||||
};
|
||||
const ctx: IMouseBindContext = { target, focus, requestedEvents };
|
||||
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
|
||||
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
|
||||
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
|
||||
mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent),
|
||||
mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent)
|
||||
};
|
||||
register(this._mouseStateService.onProtocolChange(events => {
|
||||
this._handleProtocolChange(ctx, eventListeners, events);
|
||||
}));
|
||||
// force initial onProtocolChange so we dont miss early mouse requests
|
||||
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
|
||||
|
||||
// Ensure document-level listeners are removed on dispose
|
||||
register(toDisposable(() => {
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* "Always on" event listeners.
|
||||
*/
|
||||
register(addDisposableListener(element, 'mousedown', (ev: MouseEvent) => this._handleMouseDown(ctx, ev)));
|
||||
register(addDisposableListener(element, 'wheel', (ev: WheelEvent) => this._handlePassiveWheel(ctx, ev), { passive: false }));
|
||||
register(Gesture.addTarget(target.screenElement));
|
||||
register(addDisposableListener(target.screenElement, GestureEventType.START, () => this._handleTouchStart()));
|
||||
register(addDisposableListener(target.screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleTouchChange(ctx, e)));
|
||||
}
|
||||
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return undefined;
|
||||
private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean {
|
||||
// Get mouse coordinates
|
||||
const pos = this._mouseCoordsService.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
|
||||
|
||||
let but: CoreMouseButton;
|
||||
let action: CoreMouseAction | undefined;
|
||||
switch ((ev as MouseEvent & { overrideType?: string }).overrideType || ev.type) {
|
||||
case 'mousemove':
|
||||
action = CoreMouseAction.MOVE;
|
||||
if (ev.buttons === undefined) {
|
||||
// buttons is not supported on macOS, try to get a value from button instead
|
||||
but = CoreMouseButton.NONE;
|
||||
if (ev.button !== undefined) {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
}
|
||||
} else {
|
||||
// according to MDN buttons only reports up to button 5 (AUX2)
|
||||
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
|
||||
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
|
||||
ev.buttons & 2 ? CoreMouseButton.RIGHT :
|
||||
CoreMouseButton.NONE; // fallback to NONE
|
||||
}
|
||||
break;
|
||||
case 'mouseup':
|
||||
action = CoreMouseAction.UP;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'mousedown':
|
||||
action = CoreMouseAction.DOWN;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
if (!this._mouseStateService.allowCustomWheelEvent(ev as WheelEvent)) {
|
||||
return false;
|
||||
}
|
||||
const deltaY = (ev as WheelEvent).deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
const lines = this._consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
this._renderService?.dimensions?.device?.cell?.height,
|
||||
this._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
return false;
|
||||
}
|
||||
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
but = CoreMouseButton.WHEEL;
|
||||
break;
|
||||
default:
|
||||
// dont handle other event types by accident
|
||||
return false;
|
||||
}
|
||||
|
||||
// exit if we cannot determine valid button/action values
|
||||
// do nothing for higher buttons than wheel
|
||||
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._triggerMouseEvent({
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: but,
|
||||
action,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMouseUp(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
this._sendEvent(ctx, ev);
|
||||
if (!ev.buttons) {
|
||||
// if no other button is held remove global handlers
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleWheel(ctx: IMouseBindContext, ev: WheelEvent): false {
|
||||
this._sendEvent(ctx, ev);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
private _handleMouseDrag(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
// deal only with move while a button is held
|
||||
if (ev.buttons) {
|
||||
this._sendEvent(ctx, ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMouseMove(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
// deal only with move without any button
|
||||
if (!ev.buttons) {
|
||||
this._sendEvent(ctx, ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMouseDown(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
ev.preventDefault();
|
||||
ctx.focus();
|
||||
|
||||
// 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._mouseStateService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendEvent(ctx, ev);
|
||||
|
||||
// Register additional global handlers which should keep reporting outside
|
||||
// of the terminal element.
|
||||
// Note: Other emulators also do this for 'mousedown' while a button
|
||||
// is held, we currently limit 'mousedown' to the terminal only.
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
|
||||
// do nothing, if app side handles wheel itself
|
||||
if (ctx.requestedEvents.wheel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._mouseStateService.allowCustomWheelEvent(ev)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._bufferService.buffer.hasScrollback) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
|
||||
// events are not enabled.
|
||||
// This used implementation used get the actual lines/partial lines scrolled from the
|
||||
// viewport but since moving to the new viewport implementation has been simplified to
|
||||
// simply send a single up or down sequence.
|
||||
|
||||
// Do nothing if there's no vertical scroll
|
||||
const deltaY = ev.deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = this._consumeWheelEvent(
|
||||
ev,
|
||||
this._renderService?.dimensions?.device?.cell?.height,
|
||||
this._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
this._coreService.triggerDataEvent(sequence, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTouchStart(): void {
|
||||
this._touchScrollAccumulator = 0;
|
||||
}
|
||||
|
||||
private _handleTouchChange(ctx: IMouseBindContext, e: IGestureEvent): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// When mouse protocol has wheel events active, send as mouse wheel events.
|
||||
if (ctx.requestedEvents.wheel) {
|
||||
this._handleTouchScrollAsWheel(ctx, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// When in alt buffer (no scrollback), send up/down key sequences.
|
||||
if (!this._bufferService.buffer.hasScrollback) {
|
||||
this._handleTouchScrollAsKeys(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal scrollback: delegate to viewport scrolling when available.
|
||||
ctx.target.handleTouchScroll?.(e.translationY);
|
||||
}
|
||||
|
||||
private _handleTouchScrollAsKeys(e: IGestureEvent): void {
|
||||
const cellHeight = this._renderService?.dimensions.css.cell.height;
|
||||
if (!cellHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= e.translationY;
|
||||
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
|
||||
if (lines === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= lines * cellHeight;
|
||||
const sequence = C0.ESC
|
||||
+ (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[')
|
||||
+ (lines < 0 ? 'A' : 'B');
|
||||
for (let i = 0; i < Math.abs(lines); i++) {
|
||||
this._coreService.triggerDataEvent(sequence, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTouchScrollAsWheel(ctx: IMouseBindContext, e: IGestureEvent): void {
|
||||
const cellHeight = this._renderService?.dimensions.css.cell.height;
|
||||
if (!cellHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= e.translationY;
|
||||
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
|
||||
if (lines === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= lines * cellHeight;
|
||||
const pos = this._mouseCoordsService.getMouseReportCoords(e, ctx.target.screenElement);
|
||||
if (!pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < Math.abs(lines); i++) {
|
||||
this._triggerMouseEvent({
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: CoreMouseButton.WHEEL,
|
||||
action: lines < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._lastEvent = null;
|
||||
this._wheelPartialScroll = 0;
|
||||
this._touchScrollAccumulator = 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._explainEvents(events));
|
||||
}
|
||||
element.classList.add('enable-mouse-events');
|
||||
this._selectionService.disable();
|
||||
} else {
|
||||
this._logService.debug('Unbinding from mouse events.');
|
||||
element.classList.remove('enable-mouse-events');
|
||||
this._selectionService.enable();
|
||||
}
|
||||
|
||||
// add/remove handlers from requestedEvents
|
||||
if (!(events & CoreMouseEventType.MOVE)) {
|
||||
if (requestedEvents.mousemove) {
|
||||
element.removeEventListener('mousemove', requestedEvents.mousemove);
|
||||
}
|
||||
requestedEvents.mousemove = null;
|
||||
} else if (!requestedEvents.mousemove) {
|
||||
element.addEventListener('mousemove', eventListeners.mousemove);
|
||||
requestedEvents.mousemove = eventListeners.mousemove;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.WHEEL)) {
|
||||
if (requestedEvents.wheel) {
|
||||
element.removeEventListener('wheel', requestedEvents.wheel);
|
||||
}
|
||||
requestedEvents.wheel = null;
|
||||
} else if (!requestedEvents.wheel) {
|
||||
element.addEventListener('wheel', eventListeners.wheel, { passive: false });
|
||||
requestedEvents.wheel = eventListeners.wheel;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.UP)) {
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
requestedEvents.mouseup = null;
|
||||
} else {
|
||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.DRAG)) {
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
requestedEvents.mousedrag = null;
|
||||
} else {
|
||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { SelectionService, SelectionMode } from './SelectionService';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test';
|
||||
import { MockBufferService, MockOptionsService, MockCoreService, createCellData } from 'common/TestUtils.test';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { MockCoreBrowserService, MockMouseService, MockRenderService } from 'browser/TestUtils.test';
|
||||
@@ -57,14 +57,14 @@ describe('SelectionService', () => {
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||
result.setCell(i, createCellData(0, text.charAt(i), 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine(chars.length);
|
||||
chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)])));
|
||||
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { moveToCellSequence } from 'browser/input/MoveToCell';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { IBufferLine, ICellData, IDisposable } from 'common/Types';
|
||||
@@ -126,7 +126,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
private readonly _linkifier: ILinkifier2,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
@@ -395,7 +395,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined {
|
||||
const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
||||
const coords = this._mouseCoordsService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -715,7 +715,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
|
||||
if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
|
||||
const coordinates = this._mouseService.getCoords(
|
||||
const coordinates = this._mouseCoordsService.getCoords(
|
||||
event,
|
||||
this._element,
|
||||
this._bufferService.cols,
|
||||
|
||||
@@ -49,14 +49,28 @@ export interface ICoreBrowserService {
|
||||
readonly dpr: number;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
export interface IMouseService {
|
||||
export const IMouseCoordsService = createDecorator<IMouseCoordsService>('MouseCoordsService');
|
||||
export interface IMouseCoordsService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
|
||||
getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
export interface IMouseService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void;
|
||||
reset(): void;
|
||||
}
|
||||
export interface IMouseServiceTarget {
|
||||
element: HTMLElement;
|
||||
screenElement: HTMLElement;
|
||||
document: Document;
|
||||
handleTouchScroll?(amount: number): void;
|
||||
}
|
||||
|
||||
export const IRenderService = createDecorator<IRenderService>('RenderService');
|
||||
export interface IRenderService extends IDisposable {
|
||||
serviceBrand: undefined;
|
||||
|
||||
Reference in New Issue
Block a user