Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce mouse service #2237

Merged
merged 2 commits into from
Jun 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/MouseZoneManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { ITerminal, IMouseZoneManager, IMouseZone } from './Types';
import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IMouseService } from 'browser/services/Services';

const HOVER_DURATION = 500;

Expand All @@ -31,7 +32,8 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _initialSelectionLength: number;

constructor(
private _terminal: ITerminal
private _terminal: ITerminal,
private _mouseService: IMouseService
) {
super();

Expand Down Expand Up @@ -203,7 +205,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
}

private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.cols, this._terminal.rows);
const coords = this._mouseService.getCoords(e, this._terminal.screenElement, this._terminal.cols, this._terminal.rows);
if (!coords) {
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions src/SelectionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { MockTerminal } from './TestUtils.test';
import { MockOptionsService, MockBufferService } from 'common/TestUtils.test';
import { BufferLine } from 'common/buffer/BufferLine';
import { IBufferService } from 'common/services/Services';
import { MockCharSizeService } from 'browser/TestUtils.test';
import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test';
import { CellData } from 'common/buffer/CellData';

class TestMockTerminal extends MockTerminal {
Expand All @@ -26,7 +26,7 @@ class TestSelectionManager extends SelectionManager {
terminal: ITerminal,
bufferService: IBufferService
) {
super(terminal, new MockCharSizeService(10, 10), bufferService);
super(terminal, new MockCharSizeService(10, 10), bufferService, new MockMouseService());
}

public get model(): SelectionModel { return this._model; }
Expand Down
17 changes: 9 additions & 8 deletions src/SelectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
import { ITerminal, ISelectionManager, ISelectionRedrawRequestEvent } from './Types';
import { IBuffer } from 'common/buffer/Types';
import { IBufferLine } from 'common/Types';
import { MouseHelper } from 'browser/input/MouseHelper';
import * as Browser from 'common/Platform';
import { SelectionModel } from './SelectionModel';
import { AltClickHandler } from './handlers/AltClickHandler';
import { CellData } from 'common/buffer/CellData';
import { IDisposable } from 'xterm';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
import { ICharSizeService, IMouseService } from 'browser/services/Services';
import { IBufferService } from 'common/services/Services';
import { getCoordsRelativeToElement } from 'browser/input/Mouse';

/**
* The number of pixels the mouse needs to be above or below the viewport in
Expand Down Expand Up @@ -118,9 +118,10 @@ export class SelectionManager implements ISelectionManager {
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }

constructor(
private _terminal: ITerminal,
private _charSizeService: ICharSizeService,
bufferService: IBufferService
private readonly _terminal: ITerminal,
private readonly _charSizeService: ICharSizeService,
readonly bufferService: IBufferService,
private readonly _mouseService: IMouseService
) {
this._initListeners();
this.enable();
Expand Down Expand Up @@ -357,7 +358,7 @@ export class SelectionManager implements ISelectionManager {
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true);
const coords = this._mouseService.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
Expand All @@ -377,7 +378,7 @@ export class SelectionManager implements ISelectionManager {
* @param event The mouse event.
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
let offset = getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
const terminalHeight = this._terminal.rows * Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight);
if (offset >= 0 && offset <= terminalHeight) {
return 0;
Expand Down Expand Up @@ -654,7 +655,7 @@ export class SelectionManager implements ISelectionManager {
this._removeMouseDownListeners();

if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
(new AltClickHandler(event, this._terminal)).move();
(new AltClickHandler(event, this._terminal, this._mouseService)).move();
} else if (this.hasSelection) {
this._onSelectionChange.fire();
}
Expand Down
27 changes: 14 additions & 13 deletions src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import { SelectionManager } from './SelectionManager';
import * as Browser from 'common/Platform';
import { addDisposableDomListener } from 'browser/Lifecycle';
import * as Strings from './browser/LocalizableStrings';
import { MouseHelper } from 'browser/input/MouseHelper';
import { SoundManager } from './SoundManager';
import { MouseZoneManager } from './MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
Expand All @@ -50,12 +49,13 @@ import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { OptionsService } from 'common/services/OptionsService';
import { ICharSizeService } from 'browser/services/Services';
import { ICharSizeService, IRenderService, IMouseService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
import { Disposable } from 'common/Lifecycle';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { Attributes } from 'common/buffer/Constants';
import { MouseService } from 'browser/services/MouseService';

// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
Expand Down Expand Up @@ -111,7 +111,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp

// browser services
private _charSizeService: ICharSizeService;
private _renderService: RenderService;
private _renderService: IRenderService;
private _mouseService: IMouseService;

// modes
public applicationKeypad: boolean;
Expand Down Expand Up @@ -177,7 +178,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public viewport: IViewport;
private _compositionHelper: ICompositionHelper;
private _mouseZoneManager: IMouseZoneManager;
public mouseHelper: MouseHelper;
private _accessibilityManager: AccessibilityManager;
private _colorManager: ColorManager;
private _theme: ITheme;
Expand Down Expand Up @@ -591,11 +591,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.screenElement.appendChild(this._helperContainer);
fragment.appendChild(this.screenElement);

this._mouseZoneManager = new MouseZoneManager(this);
this.register(this._mouseZoneManager);
this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
this.linkifier.attachToDom(this._mouseZoneManager);

this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
this.textarea.setAttribute('aria-label', Strings.promptLabel);
Expand Down Expand Up @@ -628,6 +623,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._renderService.onRender(e => this._onRender.fire(e));
this.onResize(e => this._renderService.resize(e.cols, e.rows));

this._mouseService = new MouseService(this._renderService, this._charSizeService);

this._mouseZoneManager = new MouseZoneManager(this, this._mouseService);
this.register(this._mouseZoneManager);
this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
this.linkifier.attachToDom(this._mouseZoneManager);

this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this._renderService.dimensions, this._charSizeService);
this.viewport.onThemeChange(this._colorManager.colors);
this.register(this.viewport);
Expand All @@ -638,7 +640,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.register(this.onFocus(() => this._renderService.onFocus()));
this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea()));

this.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService);
this.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService, this._mouseService);
this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire()));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
this.register(this.selectionManager.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
Expand All @@ -656,7 +658,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));

this.mouseHelper = new MouseHelper(this._renderService, this._charSizeService);
// apply mouse event classes set by escape codes before terminal was attached
this.element.classList.toggle('enable-mouse-events', this.mouseEvents);
if (this.mouseEvents) {
Expand Down Expand Up @@ -738,7 +739,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
button = getButton(ev);

// get mouse coordinates
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
if (!pos) return;

sendEvent(button, pos);
Expand All @@ -764,7 +765,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void {
let button = pressed;
const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
if (!pos) return;

// buttons marked as motions
Expand Down
3 changes: 1 addition & 2 deletions src/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as Browser from 'common/Platform';
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
import { Terminal } from './Terminal';
import { AttributeData } from 'common/buffer/AttributeData';
import { IColorManager, IColorSet, IMouseHelper } from 'browser/Types';
import { IColorManager, IColorSet } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { EventEmitter } from 'common/EventEmitter';

Expand Down Expand Up @@ -122,7 +122,6 @@ export class MockTerminal implements ITerminal {
throw new Error('Method not implemented.');
}
bracketedPasteMode: boolean;
mouseHelper: IMouseHelper;
renderer: IRenderer;
linkifier: ILinkifier;
isFocused: boolean;
Expand Down
3 changes: 1 addition & 2 deletions src/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { ICharset, IAttributeData, CharData } from 'common/Types';
import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IColorSet, IMouseHelper } from 'browser/Types';
import { IColorSet } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';

Expand Down Expand Up @@ -204,7 +204,6 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
buffer: IBuffer;
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
viewport: IViewport;
bracketedPasteMode: boolean;
applicationCursor: boolean;
Expand Down
12 changes: 11 additions & 1 deletion src/browser/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@
*/

import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
import { ICharSizeService, IMouseService } from 'browser/services/Services';

export class MockCharSizeService implements ICharSizeService {
get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
onCharSizeChange: IEvent<void> = new EventEmitter<void>().event;
constructor(public width: number, public height: number) {}
measure(): void {}
}

export class MockMouseService implements IMouseService {
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
throw new Error('Not implemented');
}

public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined {
throw new Error('Not implemented');
}
}
5 changes: 0 additions & 5 deletions src/browser/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,3 @@ export interface IColorSet {
selection: IColor;
ansi: IColor[];
}

export interface IMouseHelper {
getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number | undefined, y: number | undefined };
}
40 changes: 40 additions & 0 deletions src/browser/input/Mouse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

import jsdom = require('jsdom');
import { assert } from 'chai';
import { getCoords } from 'browser/input/Mouse';

const CHAR_WIDTH = 10;
const CHAR_HEIGHT = 20;

describe('Mouse getCoords', () => {
let document: Document;

beforeEach(() => {
document = new jsdom.JSDOM('').window.document;
});

it('should return the cell that was clicked', () => {
let coords: [number, number] | undefined;
coords = getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 1]);
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 1]);
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 2]);
coords = getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [2, 1]);
});

it('should ensure the coordinates are returned within the terminal bounds', () => {
let coords: [number, number] | undefined;
coords = getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 1]);
// Event are double the cols/rows
coords = getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
});
});
58 changes: 58 additions & 0 deletions src/browser/input/Mouse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
const rect = element.getBoundingClientRect();
return [event.clientX - rect.left, event.clientY - rect.top];
}

/**
* Gets coordinates within the terminal for a particular mouse event. The result
* is returned as an array in the form [x, y] instead of an object as it's a
* little faster and this function is used in some low level code.
* @param event The mouse event.
* @param element The terminal's container element.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows n the terminal.
* @param isSelection Whether the request is for the selection or not. This will
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
// Coordinates cannot be measured if there are no valid
if (!hasValidCharSize) {
return undefined;
}

const coords = getCoordsRelativeToElement(event, element);
if (!coords) {
return undefined;
}

coords[0] = Math.ceil((coords[0] + (isSelection ? actualCellWidth / 2 : 0)) / actualCellWidth);
coords[1] = Math.ceil(coords[1] / actualCellHeight);

// Ensure coordinates are within the terminal viewport. Note that selections
// need an addition point of precision to cover the end point (as characters
// cover half of one char and half of the next).
coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0));
coords[1] = Math.min(Math.max(coords[1], 1), rowCount);

return coords;
}

/**
* Gets coordinates within the terminal for a particular mouse event, wrapping
* them to the bounds of the terminal and adding 32 to both the x and y values
* as expected by xterm.
*/
export function getRawByteCoords(coords: [number, number] | undefined): { x: number, y: number } | undefined {
if (!coords) {
return undefined;
}

// xterm sends raw bytes and starts at 32 (SP) for each.
return { x: coords[0] + 32, y: coords[1] + 32 };
}