From 79c07d436e5f5d6a5cb1f875dbafb7b51137b1f9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 11:53:19 +0900 Subject: [PATCH 1/6] Fix selection horizontal offset Fixes #1000 --- src/utils/Mouse.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index 7bc39b7f..66e7f190 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -49,7 +49,8 @@ export function getCoords(event: {pageX: number, pageY: number}, element: HTMLEl } // Convert to cols/rows. - coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width); + const flooredCharWidth = Math.floor(charMeasure.width); + coords[0] = Math.ceil((coords[0] + (isSelection ? flooredCharWidth / 2 : 0)) / flooredCharWidth); coords[1] = Math.ceil(coords[1] / Math.ceil(charMeasure.height * lineHeight)); // Ensure coordinates are within the terminal viewport. From 0b54df14e2cda3f1c160e8031e8238fe3a81d1c7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 12:16:27 +0900 Subject: [PATCH 2/6] Remove unnecessary rounding --- src/renderer/BaseRenderLayer.ts | 40 +++++++++++---------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 8f655de5..16de826e 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -88,16 +88,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(terminal: ITerminal): void; - /** - * Gets the left position of a cell. Since character width is stored as a - * float in order to prevent bad letter spacing, drawing shapes in the cell - * need to be rounded. - * @param x The column of the cell. - */ - private _getCellLeft(x: number): number { - return Math.round(x * this._scaledCharWidth); - } - /** * Fills 1+ cells completely. This uses the existing fillStyle on the context. * @param x The column to start at. @@ -106,12 +96,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param height The number of rows to fill. */ protected fillCells(x: number, y: number, width: number, height: number): void { - const cellLeft = this._getCellLeft(x); this._ctx.fillRect( - cellLeft, - y * this._scaledLineHeight, - this._getCellLeft(x + width) - cellLeft, - height * this._scaledLineHeight); + x * this._scaledCharWidth, + y * this._scaledLineHeight, + width * this._scaledCharWidth, + height * this._scaledLineHeight); } /** @@ -121,11 +110,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { - const cellLeft = this._getCellLeft(x); this._ctx.fillRect( - cellLeft, + x * this._scaledCharWidth, (y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - this._getCellLeft(x + width) - cellLeft, + width * this._scaledCharWidth, window.devicePixelRatio); } @@ -137,7 +125,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( - this._getCellLeft(x), + x * this._scaledCharWidth, y * this._scaledLineHeight, window.devicePixelRatio, this._scaledLineHeight); @@ -150,12 +138,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { - const cellLeft = this._getCellLeft(x); this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( - cellLeft + window.devicePixelRatio / 2, + x * this._scaledCharWidth + window.devicePixelRatio / 2, y * this._scaledLineHeight + (window.devicePixelRatio / 2), - this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio, + width * this._scaledCharWidth - window.devicePixelRatio, (height * this._scaledLineHeight) - window.devicePixelRatio); } @@ -179,19 +166,18 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param height The number of rows to clear. */ protected clearCells(x: number, y: number, width: number, height: number): void { - const cellLeft = this._getCellLeft(x); if (this._alpha) { this._ctx.clearRect( - cellLeft, + x * this._scaledCharWidth, y * this._scaledLineHeight, - this._getCellLeft(x + width) - cellLeft, + width * this._scaledCharWidth, height * this._scaledLineHeight); } else { this._ctx.fillStyle = this._colors.background; this._ctx.fillRect( - cellLeft, + x * this._scaledCharWidth, y * this._scaledLineHeight, - this._getCellLeft(x + width) - cellLeft, + width * this._scaledCharWidth, height * this._scaledLineHeight); } } From 8243eef67ca6bc81a77d76b95cc4fd78d3ba8e6d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 12:54:20 +0900 Subject: [PATCH 3/6] Properly clear links MouzeZoneManager rows are not 0-based Fixes #990 --- src/input/MouseZoneManager.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index c7feed2a..026e79c2 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -50,10 +50,16 @@ export class MouseZoneManager implements IMouseZoneManager { return; } + // Clear all if start/end weren't set + if (!end) { + start = 0; + end = this._terminal.rows - 1; + } + // Iterate through zones and clear them out if they're within the range for (let i = 0; i < this._zones.length; i++) { const zone = this._zones[i]; - if (zone.y >= start && zone.y <= end) { + if (zone.y > start && zone.y <= end + 1) { if (this._currentZone && this._currentZone === zone) { this._currentZone.leaveCallback(); this._currentZone = null; From 13d46476c896c7794deacd89f4d012c5ae25ddce Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 28 Sep 2017 14:52:56 -0400 Subject: [PATCH 4/6] Add null check when fetching line string See Microsoft/vscode#34466 --- src/Buffer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Buffer.ts b/src/Buffer.ts index 2d23980d..548fba71 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -211,6 +211,9 @@ export class Buffer implements IBuffer { let widthAdjustedStartCol = startCol; let widthAdjustedEndCol = endCol; const line = this.lines.get(lineIndex); + if (!line) { + return ''; + } for (let i = 0; i < line.length; i++) { const char = line[i]; lineString += char[CHAR_DATA_CHAR_INDEX]; From a8174398f593d7cc3360f203c140e724ec9e6a21 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 28 Sep 2017 17:23:45 -0400 Subject: [PATCH 5/6] Calculate mouse coordinates using correct char dimensions Previously the Mouse class was using raw CSS pixels, we need CSS pixels based on the amount of space the cells take up on the canvas See Microsoft/vscode#35114 --- src/Interfaces.ts | 6 +++ src/SelectionManager.ts | 6 +-- src/Terminal.ts | 10 ++-- src/input/MouseZoneManager.ts | 3 +- src/renderer/Interfaces.ts | 2 + src/renderer/Renderer.ts | 13 +++++- src/utils/Mouse.test.ts | 63 ------------------------- src/utils/Mouse.ts | 83 --------------------------------- src/utils/MouseHelper.test.ts | 70 ++++++++++++++++++++++++++++ src/utils/MouseHelper.ts | 86 +++++++++++++++++++++++++++++++++++ src/utils/TestUtils.test.ts | 3 +- 11 files changed, 188 insertions(+), 157 deletions(-) delete mode 100644 src/utils/Mouse.test.ts delete mode 100644 src/utils/Mouse.ts create mode 100644 src/utils/MouseHelper.test.ts create mode 100644 src/utils/MouseHelper.ts diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 513deede..2c77e8d3 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -47,6 +47,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement options: ITerminalOptions; buffers: IBufferSet; isFocused: boolean; + mouseHelper: IMouseHelper; /** * Emit the 'data' event and populate the given data. @@ -174,6 +175,11 @@ export interface IBufferSet { activateAltBuffer(): void; } +export interface IMouseHelper { + getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; + getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; +} + export interface IViewport { syncScrollArea(): void; onWheel(ev: WheelEvent): void; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 28ed1522..23fe729d 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,7 +3,7 @@ * @license MIT */ -import * as Mouse from './utils/Mouse'; +import { MouseHelper } from './utils/MouseHelper'; import * as Browser from './utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; @@ -273,7 +273,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = Mouse.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); + const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -292,7 +292,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = Mouse.getCoordsRelativeToElement(event, this._terminal.element)[1]; + let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1]; const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; diff --git a/src/Terminal.ts b/src/Terminal.ts index 52a9142a..8486f265 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -36,9 +36,8 @@ import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; -import * as Mouse from './utils/Mouse'; +import { MouseHelper } from './utils/MouseHelper'; import { CHARSETS } from './Charsets'; -import { getRawByteCoords } from './utils/Mouse'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces'; import { BellSound } from './utils/Sounds'; @@ -200,6 +199,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private compositionHelper: ICompositionHelper; public charMeasure: CharMeasure; private _mouseZoneManager: IMouseZoneManager; + public mouseHelper: MouseHelper; public cols: number; public rows: number; @@ -652,6 +652,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }); this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + this.mouseHelper = new MouseHelper(this.renderer); + // Measure the character size this.charMeasure.measure(this.options); @@ -722,7 +724,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT button = getButton(ev); // get mouse coordinates - pos = getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -748,7 +750,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - let pos = getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; // buttons marked as motions diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index c7feed2a..f7a1ff61 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -5,7 +5,6 @@ import { IMouseZoneManager, IMouseZone } from './Interfaces'; import { ITerminal } from '../Interfaces'; -import { getCoords } from '../utils/Mouse'; const HOVER_DURATION = 500; @@ -144,7 +143,7 @@ export class MouseZoneManager implements IMouseZoneManager { } private _findZoneEventAt(e: MouseEvent): IMouseZone { - const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); + const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); for (let i = 0; i < this._zones.length; i++) { const zone = this._zones[i]; if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) { diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index bb9e36be..74400b8c 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -92,4 +92,6 @@ export interface IRenderDimensions { scaledCanvasHeight: number; canvasWidth: number; canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 1bdd3b4a..168d6dc8 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -45,7 +45,9 @@ export class Renderer extends EventEmitter implements IRenderer { scaledCanvasWidth: null, scaledCanvasHeight: null, canvasWidth: null, - canvasHeight: null + canvasHeight: null, + actualCellWidth: null, + actualCellHeight: null }; this._devicePixelRatio = window.devicePixelRatio; } @@ -112,6 +114,15 @@ export class Renderer extends EventEmitter implements IRenderer { this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); + // Get the _actual_ dimensions of an individual cell. This needs to be + // derived from the canvasWidth/Height calculated above which takes into + // account window.devicePixelRatio. CharMeasure.width/height by itself is + // insufficient when the page is not at 100% zoom level as CharMeasure is + // measured in CSS pixels, but the actual char size on the canvas can + // differ. + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + // Resize all render layers this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); diff --git a/src/utils/Mouse.test.ts b/src/utils/Mouse.test.ts deleted file mode 100644 index b96dd68a..00000000 --- a/src/utils/Mouse.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import jsdom = require('jsdom'); -import { assert } from 'chai'; -import { getCoords } from './Mouse'; -import { MockCharMeasure } from './TestUtils.test'; - -const CHAR_WIDTH = 10; -const CHAR_HEIGHT = 20; - -describe('getCoords', () => { - let dom: jsdom.JSDOM; - let window: Window; - let document: Document; - - let charMeasure: MockCharMeasure; - - beforeEach(() => { - dom = new jsdom.JSDOM(''); - window = dom.window; - document = window.document; - charMeasure = new MockCharMeasure(); - charMeasure.width = CHAR_WIDTH; - charMeasure.height = CHAR_HEIGHT; - }); - - describe('when charMeasure is not initialized', () => { - it('should return null', () => { - charMeasure = new MockCharMeasure(); - assert.equal(getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 1, 10, 10), null); - }); - }); - - describe('when pageX/pageY are not supported', () => { - it('should return null', () => { - assert.equal(getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 1, 10, 10), null); - }); - }); - - it('should return the cell that was clicked', () => { - let coords: [number, number]; - coords = getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10); - assert.deepEqual(coords, [1, 1]); - coords = getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); - assert.deepEqual(coords, [1, 1]); - coords = getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10); - assert.deepEqual(coords, [1, 2]); - coords = getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); - assert.deepEqual(coords, [2, 1]); - }); - - it('should ensure the coordinates are returned within the terminal bounds', () => { - let coords: [number, number]; - coords = getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10); - assert.deepEqual(coords, [1, 1]); - // Event are double the cols/rows - coords = getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10); - assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); - }); -}); diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts deleted file mode 100644 index 66e7f190..00000000 --- a/src/utils/Mouse.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ICharMeasure } from '../Interfaces'; - -export function getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] { - // Ignore browsers that don't support MouseEvent.pageX - if (event.pageX == null) { - return null; - } - - let x = event.pageX; - let y = event.pageY; - - // Converts the coordinates from being relative to the document to being - // relative to the terminal. - while (element) { - x -= element.offsetLeft; - y -= element.offsetTop; - element = 'offsetParent' in element ? element.offsetParent : element.parentElement; - } - return [x, y]; -} - -/** - * 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 charMeasure The char measure object used to determine character sizes. - * @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: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { - // Coordinates cannot be measured if charMeasure has not been initialized - if (!charMeasure.width || !charMeasure.height) { - return null; - } - - const coords = getCoordsRelativeToElement(event, element); - if (!coords) { - return null; - } - - // Convert to cols/rows. - const flooredCharWidth = Math.floor(charMeasure.width); - coords[0] = Math.ceil((coords[0] + (isSelection ? flooredCharWidth / 2 : 0)) / flooredCharWidth); - coords[1] = Math.ceil(coords[1] / Math.ceil(charMeasure.height * lineHeight)); - - // Ensure coordinates are within the terminal viewport. - coords[0] = Math.min(Math.max(coords[0], 1), colCount); - 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. - * @param event The mouse event. - * @param element The terminal's container element. - * @param charMeasure The char measure object used to determine character sizes. - * @param colCount The number of columns in the terminal. - * @param rowCount The number of rows in the terminal. - */ -export function getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { - const coords = getCoords(event, element, charMeasure, lineHeight, colCount, rowCount); - let x = coords[0]; - let y = coords[1]; - - // xterm sends raw bytes and starts at 32 (SP) for each. - x += 32; - y += 32; - - return { x, y }; -} diff --git a/src/utils/MouseHelper.test.ts b/src/utils/MouseHelper.test.ts new file mode 100644 index 00000000..ac3137f4 --- /dev/null +++ b/src/utils/MouseHelper.test.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { MouseHelper } from './MouseHelper'; +import { MockCharMeasure, MockRenderer } from './TestUtils.test'; + +const CHAR_WIDTH = 10; +const CHAR_HEIGHT = 20; + +describe('MouseHelper.getCoords', () => { + let dom: jsdom.JSDOM; + let window: Window; + let document: Document; + let mouseHelper: MouseHelper; + + let charMeasure: MockCharMeasure; + + beforeEach(() => { + dom = new jsdom.JSDOM(''); + window = dom.window; + document = window.document; + charMeasure = new MockCharMeasure(); + charMeasure.width = CHAR_WIDTH; + charMeasure.height = CHAR_HEIGHT; + const renderer = new MockRenderer(); + renderer.dimensions = { + actualCellWidth: CHAR_WIDTH, + actualCellHeight: CHAR_HEIGHT + }; + mouseHelper = new MouseHelper(renderer); + }); + + describe('when charMeasure is not initialized', () => { + it('should return null', () => { + charMeasure = new MockCharMeasure(); + assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 1, 10, 10), null); + }); + }); + + describe('when pageX/pageY are not supported', () => { + it('should return null', () => { + assert.equal(mouseHelper.getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 1, 10, 10), null); + }); + }); + + it('should return the cell that was clicked', () => { + let coords: [number, number]; + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 2]); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [2, 1]); + }); + + it('should ensure the coordinates are returned within the terminal bounds', () => { + let coords: [number, number]; + coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + // Event are double the cols/rows + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); + }); +}); diff --git a/src/utils/MouseHelper.ts b/src/utils/MouseHelper.ts new file mode 100644 index 00000000..75ea675c --- /dev/null +++ b/src/utils/MouseHelper.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharMeasure } from '../Interfaces'; +import { IRenderer } from '../renderer/Interfaces'; + +export class MouseHelper { + constructor(private _renderer: IRenderer) {} + + public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] { + // Ignore browsers that don't support MouseEvent.pageX + if (event.pageX == null) { + return null; + } + + let x = event.pageX; + let y = event.pageY; + + // Converts the coordinates from being relative to the document to being + // relative to the terminal. + while (element) { + x -= element.offsetLeft; + y -= element.offsetTop; + element = 'offsetParent' in element ? element.offsetParent : element.parentElement; + } + return [x, y]; + } + + /** + * 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 charMeasure The char measure object used to determine character sizes. + * @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. + */ + public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { + // Coordinates cannot be measured if charMeasure has not been initialized + if (!charMeasure.width || !charMeasure.height) { + return null; + } + + const coords = MouseHelper.getCoordsRelativeToElement(event, element); + if (!coords) { + return null; + } + + coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderer.dimensions.actualCellWidth / 2 : 0)) / this._renderer.dimensions.actualCellWidth); + coords[1] = Math.ceil(coords[1] / this._renderer.dimensions.actualCellHeight); + + // Ensure coordinates are within the terminal viewport. + coords[0] = Math.min(Math.max(coords[0], 1), colCount); + 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. + * @param event The mouse event. + * @param element The terminal's container element. + * @param charMeasure The char measure object used to determine character sizes. + * @param colCount The number of columns in the terminal. + * @param rowCount The number of rows in the terminal. + */ + public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { + const coords = this.getCoords(event, element, charMeasure, lineHeight, colCount, rowCount); + let x = coords[0]; + let y = coords[1]; + + // xterm sends raw bytes and starts at 32 (SP) for each. + x += 32; + y += 32; + + return { x, y }; + } +} diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index beed0aaa..26573319 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier } from '../Interfaces'; +import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper } from '../Interfaces'; import { LineData } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from './Browser'; import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { + mouseHelper: IMouseHelper; renderer: IRenderer; linkifier: ILinkifier; isFocused: boolean; From 499c8288ebac90ef656359c62dbdbd70eb685ee1 Mon Sep 17 00:00:00 2001 From: Rick Baker Date: Fri, 29 Sep 2017 14:54:33 -0500 Subject: [PATCH 6/6] Update README.md Submitting an app for your list. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9fafe14..1551c864 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - +- [**DevOps Helper**](https://github.com/ricktbaker/devops_helper) DevOps Helper tool to make life easier working with AWS instances across multiple organizations. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.