mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'v3' into zmodem
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+6
-4
@@ -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
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,6 @@ export interface IRenderDimensions {
|
||||
scaledCanvasHeight: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
actualCellWidth: number;
|
||||
actualCellHeight: number;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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 ? <HTMLElement>element.offsetParent : <HTMLElement>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 };
|
||||
}
|
||||
@@ -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 = <any>{
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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 ? <HTMLElement>element.offsetParent : <HTMLElement>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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user