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
This commit is contained in:
Daniel Imms
2017-09-28 17:23:45 -04:00
parent 0dafd0a38b
commit a8174398f5
11 changed files with 188 additions and 157 deletions
-63
View File
@@ -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');
});
});
-83
View File
@@ -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 ? <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.
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 };
}
+70
View File
@@ -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');
});
});
+86
View File
@@ -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 };
}
}
+2 -1
View File
@@ -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;