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. 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]; 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..8329d0b6 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; @@ -50,10 +49,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; @@ -144,7 +149,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/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 2bf49e68..d2a704a3 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -91,16 +91,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. @@ -109,12 +99,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); } /** @@ -124,11 +113,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); } @@ -140,7 +128,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); @@ -153,12 +141,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); } @@ -182,19 +169,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); } } 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 7bc39b7f..00000000 --- a/src/utils/Mouse.ts +++ /dev/null @@ -1,82 +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. - coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width); - 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;