Merge pull request #2205 from Tyriar/char_dim_service

Create CharSizeService
This commit is contained in:
Daniel Imms
2019-06-08 15:53:25 -07:00
committed by GitHub
19 changed files with 193 additions and 237 deletions
-54
View File
@@ -1,54 +0,0 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { ICharMeasure } from './Types';
import { assert } from 'chai';
import { CharMeasure } from './CharMeasure';
describe('CharMeasure', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let container: HTMLElement;
let charMeasure: ICharMeasure;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
container = document.createElement('div');
document.body.appendChild(container);
charMeasure = new CharMeasure(document, container);
});
describe('measure', () => {
it('should have _measureElement', () => {
assert.isDefined((<any>charMeasure)._measureElement, 'new CharMeasure() should have created _measureElement');
});
it('should be performed sync', () => {
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 1, height: 1 };
};
charMeasure.measure({});
assert.equal(charMeasure.height, 1);
assert.equal(charMeasure.width, 1);
});
it('should NOT do a measure when the parent is hidden', done => {
charMeasure.measure({});
setTimeout(() => {
const firstWidth = charMeasure.width;
container.style.display = 'none';
container.style.fontSize = '2em';
charMeasure.measure({});
assert.equal(charMeasure.width, firstWidth);
done();
}, 0);
});
});
});
-58
View File
@@ -1,58 +0,0 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharMeasure, ITerminalOptions } from './Types';
import { EventEmitter2, IEvent } from 'common/EventEmitter2';
/**
* Utility class that measures the size of a character. Measurements are done in
* the DOM rather than with a canvas context because support for extracting the
* height of characters is patchy across browsers.
*/
export class CharMeasure implements ICharMeasure {
private _document: Document;
private _parentElement: HTMLElement;
private _measureElement: HTMLElement;
private _width: number;
private _height: number;
private _onCharSizeChanged = new EventEmitter2<void>();
public get onCharSizeChanged(): IEvent<void> { return this._onCharSizeChanged.event; }
constructor(document: Document, parentElement: HTMLElement) {
this._document = document;
this._parentElement = parentElement;
this._measureElement = this._document.createElement('span');
this._measureElement.classList.add('xterm-char-measure-element');
this._measureElement.textContent = 'W';
this._measureElement.setAttribute('aria-hidden', 'true');
this._parentElement.appendChild(this._measureElement);
}
public get width(): number {
return this._width;
}
public get height(): number {
return this._height;
}
public measure(options: ITerminalOptions): void {
this._measureElement.style.fontFamily = options.fontFamily;
this._measureElement.style.fontSize = `${options.fontSize}px`;
const geometry = this._measureElement.getBoundingClientRect();
// The element is likely currently display:none, we should retain the
// previous value.
if (geometry.width === 0 || geometry.height === 0) {
return;
}
const adjustedHeight = Math.ceil(geometry.height);
if (this._width !== geometry.width || this._height !== adjustedHeight) {
this._width = geometry.width;
this._height = adjustedHeight;
this._onCharSizeChanged.fire();
}
}
}
+2 -5
View File
@@ -6,6 +6,7 @@
import { assert } from 'chai';
import { CompositionHelper } from './CompositionHelper';
import { ITerminal } from './Types';
import { MockCharSizeService } from 'TestUtils.test';
describe('CompositionHelper', () => {
let terminal: ITerminal;
@@ -48,16 +49,12 @@ describe('CompositionHelper', () => {
buffer: {
isCursorInViewport: true
},
charMeasure: {
height: 10,
width: 10
},
options: {
lineHeight: 1
}
} as any;
handledText = '';
compositionHelper = new CompositionHelper(textarea, compositionView, terminal);
compositionHelper = new CompositionHelper(textarea, compositionView, terminal, new MockCharSizeService(10, 10));
});
describe('Input', () => {
+5 -3
View File
@@ -4,6 +4,7 @@
*/
import { ITerminal } from './Types';
import { ICharSizeService } from 'ui/services/Services';
interface IPosition {
start: number;
@@ -42,7 +43,8 @@ export class CompositionHelper {
constructor(
private _textarea: HTMLTextAreaElement,
private _compositionView: HTMLElement,
private _terminal: ITerminal
private _terminal: ITerminal,
private _charSizeService: ICharSizeService
) {
this._isComposing = false;
this._isSendingComposition = false;
@@ -195,9 +197,9 @@ export class CompositionHelper {
}
if (this._terminal.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight);
const cellHeight = Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight);
const cursorTop = this._terminal.buffer.y * cellHeight;
const cursorLeft = this._terminal.buffer.x * this._terminal.charMeasure.width;
const cursorLeft = this._terminal.buffer.x * this._charSizeService.width;
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
+8 -27
View File
@@ -3,62 +3,43 @@
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { MouseHelper } from './MouseHelper';
import { MockCharMeasure, MockRenderer } from './TestUtils.test';
import { MockRenderer, MockCharSizeService } 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 as any);
});
describe('when charMeasure is not initialized', () => {
it('should return null', () => {
charMeasure = new MockCharMeasure();
assert.equal(mouseHelper.getCoords({ clientX: 0, clientY: 0 }, document.createElement('div'), charMeasure, 10, 10), null);
});
mouseHelper = new MouseHelper(renderer as any, new MockCharSizeService(CHAR_WIDTH, CHAR_HEIGHT));
});
it('should return the cell that was clicked', () => {
let coords: [number, number];
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10);
assert.deepEqual(coords, [1, 1]);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10);
assert.deepEqual(coords, [1, 1]);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10);
assert.deepEqual(coords, [1, 2]);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 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({ clientX: -1, clientY: -1 }, document.createElement('div'), charMeasure, 10, 10);
coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10);
assert.deepEqual(coords, [1, 1]);
// Event are double the cols/rows
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10);
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10);
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
});
});
+9 -9
View File
@@ -3,12 +3,14 @@
* @license MIT
*/
import { ICharMeasure, IMouseHelper } from './Types';
import { IMouseHelper } from './Types';
import { RenderCoordinator } from './renderer/RenderCoordinator';
import { ICharSizeService } from 'ui/services/Services';
export class MouseHelper implements IMouseHelper {
constructor(
private _renderCoordinator: RenderCoordinator
private _renderCoordinator: RenderCoordinator,
private _charSizeService: ICharSizeService
) {
}
@@ -23,16 +25,15 @@ export class MouseHelper implements IMouseHelper {
* 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: {clientX: number, clientY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
// Coordinates cannot be measured if charMeasure has not been initialized
if (!charMeasure.width || !charMeasure.height) {
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
// Coordinates cannot be measured if there are no valid
if (!this._charSizeService.hasValidSize) {
return null;
}
@@ -59,12 +60,11 @@ export class MouseHelper implements IMouseHelper {
* 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, colCount: number, rowCount: number): { x: number, y: number } {
const coords = this.getCoords(event, element, charMeasure, colCount, rowCount);
public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } {
const coords = this.getCoords(event, element, colCount, rowCount);
let x = coords[0];
let y = coords[1];
+1 -1
View File
@@ -203,7 +203,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
}
private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.cols, this._terminal.rows);
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.cols, this._terminal.rows);
if (!coords) {
return null;
}
+4 -6
View File
@@ -4,13 +4,12 @@
*/
import { assert } from 'chai';
import { CharMeasure } from './CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { ITerminal, IBuffer } from './Types';
import { IBufferLine } from 'core/Types';
import { MockTerminal } from './TestUtils.test';
import { MockTerminal, MockCharSizeService } from './TestUtils.test';
import { BufferLine, CellData } from 'core/buffer/BufferLine';
class TestMockTerminal extends MockTerminal {
@@ -19,10 +18,9 @@ class TestMockTerminal extends MockTerminal {
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
charMeasure: CharMeasure
terminal: ITerminal
) {
super(terminal, charMeasure);
super(terminal, new MockCharSizeService(10, 10));
}
public get model(): SelectionModel { return this._model; }
@@ -52,7 +50,7 @@ describe('SelectionManager', () => {
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
buffer = terminal.buffer;
selectionManager = new TestSelectionManager(terminal, null);
selectionManager = new TestSelectionManager(terminal);
});
function stringToRow(text: string): IBufferLine {
+4 -4
View File
@@ -7,12 +7,12 @@ import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } f
import { IBufferLine } from 'core/Types';
import { MouseHelper } from './MouseHelper';
import * as Browser from 'common/Platform';
import { CharMeasure } from './CharMeasure';
import { SelectionModel } from './SelectionModel';
import { AltClickHandler } from './handlers/AltClickHandler';
import { CellData } from 'core/buffer/BufferLine';
import { IDisposable } from 'xterm';
import { EventEmitter2, IEvent } from 'common/EventEmitter2';
import { ICharSizeService } from 'ui/services/Services';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -117,7 +117,7 @@ export class SelectionManager implements ISelectionManager {
constructor(
private _terminal: ITerminal,
private _charMeasure: CharMeasure
private _charSizeService: ICharSizeService
) {
this._initListeners();
this.enable();
@@ -354,7 +354,7 @@ export class SelectionManager implements ISelectionManager {
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
@@ -375,7 +375,7 @@ export class SelectionManager implements ISelectionManager {
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
const terminalHeight = this._terminal.rows * Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight);
if (offset >= 0 && offset <= terminalHeight) {
return 0;
}
+25 -21
View File
@@ -34,7 +34,6 @@ import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './CharMeasure';
import * as Browser from 'common/Platform';
import { addDisposableDomListener } from 'ui/Lifecycle';
import * as Strings from './Strings';
@@ -55,6 +54,8 @@ import { ColorManager } from 'ui/ColorManager';
import { RenderCoordinator } from './renderer/RenderCoordinator';
import { IOptionsService } from 'common/options/Types';
import { OptionsService } from 'common/options/OptionsService';
import { ICharSizeService } from 'ui/services/Services';
import { CharSizeService } from 'ui/services/CharSizeService';
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -107,9 +108,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _customKeyEventHandler: CustomKeyEventHandler;
// services
// common services
public optionsService: IOptionsService;
// browser services
private _charSizeService: ICharSizeService;
// modes
public applicationKeypad: boolean;
public applicationCursor: boolean;
@@ -175,7 +179,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public buffers: BufferSet;
public viewport: IViewport;
private _compositionHelper: ICompositionHelper;
public charMeasure: CharMeasure;
private _mouseZoneManager: IMouseZoneManager;
public mouseHelper: MouseHelper;
private _accessibilityManager: AccessibilityManager;
@@ -360,7 +363,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// When the font changes the size of the cells may change which requires a renderer clear
if (this._renderCoordinator) {
this._renderCoordinator.clear();
this.charMeasure.measure(this.options);
}
if (this._charSizeService) {
this._charSizeService.measure();
}
break;
case 'drawBoldTextInBrightColors':
@@ -609,13 +614,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._charSizeService = new CharSizeService(this._document, this._helperContainer, this.optionsService);
this._compositionView = document.createElement('div');
this._compositionView.classList.add('composition-view');
this._compositionHelper = new CompositionHelper(this.textarea, this._compositionView, this);
this._compositionHelper = new CompositionHelper(this.textarea, this._compositionView, this, this._charSizeService);
this._helperContainer.appendChild(this._compositionView);
this.charMeasure = new CharMeasure(document, this._helperContainer);
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
@@ -625,11 +630,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._colorManager.setTheme(this._theme);
const renderer = this._createRenderer();
this._renderCoordinator = new RenderCoordinator(renderer, this.rows, this.screenElement, this.optionsService);
this._renderCoordinator = new RenderCoordinator(renderer, this.rows, this.screenElement, this.optionsService, this._charSizeService);
this._renderCoordinator.onRender(e => this._onRender.fire(e));
this.onResize(e => this._renderCoordinator.resize(e.cols, e.rows));
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure, this._renderCoordinator.dimensions);
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this._renderCoordinator.dimensions, this._charSizeService);
this.viewport.onThemeChange(this._colorManager.colors);
this.register(this.viewport);
@@ -637,10 +642,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.register(this.onResize(() => this._renderCoordinator.onResize(this.cols, this.rows)));
this.register(this.addDisposableListener('blur', () => this._renderCoordinator.onBlur()));
this.register(this.addDisposableListener('focus', () => this._renderCoordinator.onFocus()));
this.register(this.charMeasure.onCharSizeChanged(() => this._renderCoordinator.onCharSizeChanged()));
this.register(this._renderCoordinator.onDimensionsChange(() => this.viewport.syncScrollArea()));
this.selectionManager = new SelectionManager(this, this.charMeasure);
this.selectionManager = new SelectionManager(this, this._charSizeService);
this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire()));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
this.register(this.selectionManager.onRedrawRequest(e => this._renderCoordinator.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
@@ -658,7 +662,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));
this.mouseHelper = new MouseHelper(this._renderCoordinator);
this.mouseHelper = new MouseHelper(this._renderCoordinator, this._charSizeService);
// apply mouse event classes set by escape codes before terminal was attached
this.element.classList.toggle('enable-mouse-events', this.mouseEvents);
if (this.mouseEvents) {
@@ -675,7 +679,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
// Measure the character size
this.charMeasure.measure(this.options);
this._charSizeService.measure();
// Setup loop that draws to screen
this.refresh(0, this.rows - 1);
@@ -691,8 +695,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _createRenderer(): IRenderer {
switch (this.options.rendererType) {
case 'canvas': return new Renderer(this, this._colorManager.colors); break;
case 'dom': return new DomRenderer(this, this._colorManager.colors); break;
case 'canvas': return new Renderer(this, this._colorManager.colors, this._charSizeService); break;
case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService); break;
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
}
@@ -738,7 +742,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
button = getButton(ev);
// get mouse coordinates
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows);
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
if (!pos) return;
sendEvent(button, pos);
@@ -764,7 +768,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void {
let button = pressed;
const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows);
const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
if (!pos) return;
// buttons marked as motions
@@ -1731,8 +1735,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (x === this.cols && y === this.rows) {
// Check if we still need to measure the char size (fixes #785).
if (this.charMeasure && (!this.charMeasure.width || !this.charMeasure.height)) {
this.charMeasure.measure(this.options);
if (this._charSizeService && !this._charSizeService.hasValidSize) {
this._charSizeService.measure();
}
return;
}
@@ -1746,8 +1750,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.rows = y;
this.buffers.setupTabStops(this.cols);
if (this.charMeasure) {
this.charMeasure.measure(this.options);
if (this._charSizeService) {
this._charSizeService.measure();
}
this.refresh(0, this.rows - 1);
+9 -11
View File
@@ -4,7 +4,7 @@
*/
import { IRenderer, IRenderDimensions } from './renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types';
import { IBufferLine, ICellData, IAttributeData } from 'core/Types';
import { ICircularList, XtermListener } from 'common/Types';
import { Buffer } from './Buffer';
@@ -14,6 +14,7 @@ import { Terminal } from './Terminal';
import { AttributeData } from 'core/buffer/BufferLine';
import { IColorManager, IColorSet } from 'ui/Types';
import { IOptionsService } from 'common/options/Types';
import { ICharSizeService } from 'ui/services/Services';
export class TestTerminal extends Terminal {
writeSync(data: string): void {
@@ -128,7 +129,6 @@ export class MockTerminal implements ITerminal {
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
rows: number;
cols: number;
@@ -182,15 +182,6 @@ export class MockTerminal implements ITerminal {
deregisterCharacterJoiner(joinerId: number): void { }
}
export class MockCharMeasure implements ICharMeasure {
onCharSizeChanged: IEvent<void>;
width: number;
height: number;
measure(options: ITerminalOptions): void {
throw new Error('Method not implemented.');
}
}
export class MockInputHandlingTerminal implements IInputHandlingTerminal {
element: HTMLElement;
options: ITerminalOptions = {};
@@ -438,3 +429,10 @@ export class MockCompositionHelper implements ICompositionHelper {
return true;
}
}
export class MockCharSizeService implements ICharSizeService {
get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
onCharSizeChange: IEvent<void>;
constructor(public width: number, public height: number) {}
measure(): void {}
}
+2 -12
View File
@@ -199,7 +199,6 @@ export interface ILinkifierEvent {
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
@@ -284,17 +283,8 @@ export interface ILinkifierAccessor {
}
export interface IMouseHelper {
getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number };
}
export interface ICharMeasure {
width: number;
height: number;
onCharSizeChanged: IEvent<void>;
measure(options: ITerminalOptions): void;
getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number };
}
// TODO: The options that are not in the public API should be reviewed
+5 -12
View File
@@ -4,11 +4,11 @@
*/
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './CharMeasure';
import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'ui/Lifecycle';
import { IColorSet } from 'ui/Types';
import { IRenderDimensions } from './renderer/Types';
import { ICharSizeService } from 'ui/services/Services';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -33,19 +33,12 @@ export class Viewport extends Disposable implements IViewport {
private _refreshAnimationFrame: number | null = null;
private _ignoreNextScrollEvent: boolean = false;
/**
* Creates a new Viewport.
* @param _terminal The terminal this viewport belongs to.
* @param _viewportElement The DOM element acting as the viewport.
* @param _scrollArea The DOM element acting as the scroll area.
* @param _charMeasure A DOM element used to measure the character size of. the terminal.
*/
constructor(
private _terminal: ITerminal,
private _viewportElement: HTMLElement,
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure,
private _dimensions: IRenderDimensions
private _dimensions: IRenderDimensions,
private _charSizeService: ICharSizeService
) {
super();
@@ -55,7 +48,7 @@ export class Viewport extends Disposable implements IViewport {
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the CharMeasure is ready.
// Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
@@ -78,7 +71,7 @@ export class Viewport extends Disposable implements IViewport {
}
private _innerRefresh(): void {
if (this._charMeasure.height > 0) {
if (this._charSizeService.height > 0) {
this._currentRowHeight = this._dimensions.scaledCellHeight / window.devicePixelRatio;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._dimensions.canvasHeight);
-1
View File
@@ -33,7 +33,6 @@ export class AltClickHandler {
const coordinates = this._terminal.mouseHelper.getCoords(
this._mouseEvent,
this._terminal.element,
this._terminal.charMeasure,
this._terminal.cols,
this._terminal.rows,
false
+4 -1
View File
@@ -12,6 +12,7 @@ import { addDisposableDomListener } from 'ui/Lifecycle';
import { IColorSet } from 'ui/Types';
import { CharacterJoinerHandler } from '../Types';
import { IOptionsService } from 'common/options/Types';
import { ICharSizeService } from 'ui/services/Services';
export class RenderCoordinator extends Disposable {
private _renderDebouncer: RenderDebouncer;
@@ -35,7 +36,8 @@ export class RenderCoordinator extends Disposable {
private _renderer: IRenderer,
private _rowCount: number,
screenElement: HTMLElement,
optionsService: IOptionsService
optionsService: IOptionsService,
charSizeService: ICharSizeService
) {
super();
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end));
@@ -46,6 +48,7 @@ export class RenderCoordinator extends Disposable {
this.register(this._screenDprMonitor);
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
+9 -9
View File
@@ -12,6 +12,7 @@ import { LinkRenderLayer } from './LinkRenderLayer';
import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry';
import { Disposable } from 'common/Lifecycle';
import { IColorSet } from 'ui/Types';
import { ICharSizeService } from 'ui/services/Services';
export class Renderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
@@ -22,7 +23,8 @@ export class Renderer extends Disposable implements IRenderer {
constructor(
private _terminal: ITerminal,
private _colors: IColorSet
private _colors: IColorSet,
private _charSizeService: ICharSizeService
) {
super();
const allowTransparency = this._terminal.options.allowTransparency;
@@ -133,8 +135,7 @@ export class Renderer extends Disposable implements IRenderer {
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
// Perform a new measure if the CharMeasure dimensions are not yet available
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
if (!this._charSizeService.hasValidSize) {
return;
}
@@ -142,12 +143,12 @@ export class Renderer extends Disposable implements IRenderer {
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio);
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
// Calculate the scaled cell height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
@@ -181,10 +182,9 @@ export class Renderer extends Disposable implements IRenderer {
// 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.
// account window.devicePixelRatio. ICharSizeService.width/height by itself
// is insufficient when the page is not at 100% zoom level as it's 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;
}
+5 -3
View File
@@ -9,6 +9,7 @@ import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSO
import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
import { Disposable } from 'common/Lifecycle';
import { IColorSet } from 'ui/Types';
import { ICharSizeService } from 'ui/services/Services';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -41,7 +42,8 @@ export class DomRenderer extends Disposable implements IRenderer {
constructor(
private _terminal: ITerminal,
private _colors: IColorSet
private _colors: IColorSet,
private _charSizeService: ICharSizeService
) {
super();
@@ -91,8 +93,8 @@ export class DomRenderer extends Disposable implements IRenderer {
}
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
this.dimensions.scaledCharLeft = 0;
+85
View File
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IOptionsService } from 'common/options/Types';
import { IEvent, EventEmitter2 } from 'common/EventEmitter2';
import { ICharSizeService } from 'ui/services/Services';
export class CharSizeService implements ICharSizeService {
public width: number = 0;
public height: number = 0;
private _measureStrategy: IMeasureStrategy;
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
private _onCharSizeChange = new EventEmitter2<void>();
public get onCharSizeChange(): IEvent<void> { return this._onCharSizeChange.event; }
constructor(
document: Document,
parentElement: HTMLElement,
private _optionsService: IOptionsService
) {
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
}
public measure(): void {
const result = this._measureStrategy.measure();
if (result.width !== this.width || result.height !== this.height) {
this.width = result.width;
this.height = result.height;
this._onCharSizeChange.fire();
}
}
}
interface IMeasureStrategy {
measure(): IReadonlyMeasureResult;
}
interface IReadonlyMeasureResult {
readonly width: number;
readonly height: number;
}
interface IMeasureResult {
width: number;
height: number;
}
// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses ctx.measureText
class DomMeasureStrategy implements IMeasureStrategy {
private _result: IMeasureResult = { width: 0, height: 0 };
private _measureElement: HTMLElement;
constructor(
private _document: Document,
private _parentElement: HTMLElement,
private _optionsService: IOptionsService
) {
this._measureElement = this._document.createElement('span');
this._measureElement.classList.add('xterm-char-measure-element');
this._measureElement.textContent = 'W';
this._measureElement.setAttribute('aria-hidden', 'true');
this._parentElement.appendChild(this._measureElement);
}
public measure(): IReadonlyMeasureResult {
this._measureElement.style.fontFamily = this._optionsService.options.fontFamily;
this._measureElement.style.fontSize = `${this._optionsService.options.fontSize}px`;
// Note that this triggers a synchronous layout
const geometry = this._measureElement.getBoundingClientRect();
console.log('measure', geometry);
// If values are 0 then the element is likely currently display:none, in which case we should
// retain the previous value.
if (geometry.width !== 0 && geometry.height !== 0) {
this._result.width = geometry.width;
this._result.height = Math.ceil(geometry.height);
}
return this._result;
}
}
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IEvent } from 'common/EventEmitter2';
export interface ICharSizeService {
readonly width: number;
readonly height: number;
readonly hasValidSize: boolean;
readonly onCharSizeChange: IEvent<void>;
measure(): void;
}