mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge remote-tracking branch 'ups/v3' into 963_cursor_accent
This commit is contained in:
@@ -128,6 +128,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
|
||||
- [**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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ export class CompositionHelper {
|
||||
* @param {CompositionEvent} ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
console.log('compositionupdate');
|
||||
this.compositionView.textContent = ev.data;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
|
||||
+2
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
import { ILinkMatcherOptions } from './Interfaces';
|
||||
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
|
||||
import { IColorSet } from './renderer/Interfaces';
|
||||
import { IColorSet, IRenderer } from './renderer/Interfaces';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
|
||||
export interface IBrowser {
|
||||
@@ -36,6 +36,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
textarea: HTMLTextAreaElement;
|
||||
renderer: IRenderer;
|
||||
rows: number;
|
||||
cols: number;
|
||||
browser: IBrowser;
|
||||
|
||||
+23
-8
@@ -49,11 +49,16 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
private _rowsTimeoutId: number;
|
||||
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||
private _rowsToLinkify: {start: number, end: number};
|
||||
|
||||
constructor(
|
||||
protected _terminal: IBufferAccessor & IElementAccessor
|
||||
) {
|
||||
super();
|
||||
this._rowsToLinkify = {
|
||||
start: null,
|
||||
end: null
|
||||
};
|
||||
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||
}
|
||||
|
||||
@@ -76,25 +81,35 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear out any existing links
|
||||
this._mouseZoneManager.clearAll();
|
||||
// Increase range to linkify
|
||||
if (!this._rowsToLinkify.start) {
|
||||
this._rowsToLinkify.start = start;
|
||||
this._rowsToLinkify.end = end;
|
||||
} else {
|
||||
this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start;
|
||||
this._rowsToLinkify.end = this._rowsToLinkify.end < end ? this._rowsToLinkify.end : end;
|
||||
}
|
||||
|
||||
// Clear out any existing links on this row range
|
||||
this._mouseZoneManager.clearAll(start, end);
|
||||
|
||||
// Restart timer
|
||||
if (this._rowsTimeoutId) {
|
||||
clearTimeout(this._rowsTimeoutId);
|
||||
}
|
||||
this._rowsTimeoutId = setTimeout(this._linkifyRows.bind(this, start, end), Linkifier.TIME_BEFORE_LINKIFY);
|
||||
this._rowsTimeoutId = <number><any>setTimeout(() => this._linkifyRows(), Linkifier.TIME_BEFORE_LINKIFY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies
|
||||
* @param start The row to start at.
|
||||
* @param end The row to end at.
|
||||
* Linkifies the rows requested.
|
||||
*/
|
||||
private _linkifyRows(start: number, end: number): void {
|
||||
private _linkifyRows(): void {
|
||||
this._rowsTimeoutId = null;
|
||||
for (let i = start; i <= end; i++) {
|
||||
for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
|
||||
this._linkifyRow(i);
|
||||
}
|
||||
this._rowsToLinkify.start = null;
|
||||
this._rowsToLinkify.end = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -454,6 +454,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mousemove event.
|
||||
*/
|
||||
private _onMouseMove(event: MouseEvent): void {
|
||||
// If the mousemove listener is active it means that a selection is
|
||||
// currently being made, we should stop propogation to prevent mouse events
|
||||
// to be sent to the pty.
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
// Record the previous position so we know whether to redraw the selection
|
||||
// at the end.
|
||||
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
|
||||
|
||||
+12
-5
@@ -48,8 +48,11 @@ import { MouseZoneManager } from './input/MouseZoneManager';
|
||||
import { initialize as initializeCharAtlas } from './renderer/CharAtlas';
|
||||
import { IRenderer } from './renderer/Interfaces';
|
||||
|
||||
// Declare for RequireJS in loadAddon
|
||||
// Declares required for loadAddon
|
||||
declare var exports: any;
|
||||
declare var module: any;
|
||||
declare var define: any;
|
||||
declare var require: any;
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
@@ -188,7 +191,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
|
||||
private inputHandler: InputHandler;
|
||||
private parser: Parser;
|
||||
private renderer: IRenderer;
|
||||
public renderer: IRenderer;
|
||||
public selectionManager: SelectionManager;
|
||||
public linkifier: ILinkifier;
|
||||
public buffers: BufferSet;
|
||||
@@ -588,6 +591,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.syncBellSound();
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this);
|
||||
this.on('scroll', () => this._mouseZoneManager.clearAll());
|
||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||
|
||||
// Create the container that will hold helpers like the textarea for
|
||||
@@ -619,7 +623,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.charMeasure = new CharMeasure(document, this.helperContainer);
|
||||
|
||||
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
|
||||
this.charMeasure.on('charsizechanged', () => this.viewport.syncScrollArea());
|
||||
this.renderer = new Renderer(this);
|
||||
this.on('cursormove', () => this.renderer.onCursorMove());
|
||||
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
|
||||
@@ -627,6 +630,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.on('focus', () => this.renderer.onFocus());
|
||||
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
|
||||
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
|
||||
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
|
||||
|
||||
this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure);
|
||||
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
|
||||
@@ -639,7 +643,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.textarea.focus();
|
||||
this.textarea.select();
|
||||
});
|
||||
this.on('scroll', () => this.selectionManager.refresh());
|
||||
this.on('scroll', () => {
|
||||
this.viewport.syncScrollArea();
|
||||
this.selectionManager.refresh();
|
||||
});
|
||||
this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
|
||||
|
||||
// Measure the character size
|
||||
@@ -1034,7 +1041,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
*/
|
||||
private queueLinkification(start: number, end: number): void {
|
||||
if (this.linkifier) {
|
||||
this.linkifier.linkifyRows(0, this.rows);
|
||||
this.linkifier.linkifyRows(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Viewport } from './Viewport';
|
||||
import { BufferSet } from './BufferSet';
|
||||
|
||||
describe('Viewport', () => {
|
||||
let terminal;
|
||||
let viewportElement;
|
||||
let charMeasure;
|
||||
let viewport;
|
||||
let scrollAreaElement;
|
||||
|
||||
const CHARACTER_HEIGHT = 10;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = {
|
||||
rows: 0,
|
||||
ydisp: 0,
|
||||
on: () => {},
|
||||
rowContainer: {
|
||||
style: {
|
||||
lineHeight: 0
|
||||
}
|
||||
},
|
||||
selectionContainer: {
|
||||
style: {
|
||||
height: 0
|
||||
}
|
||||
},
|
||||
options: {
|
||||
scrollback: 10,
|
||||
lineHeight: 1
|
||||
}
|
||||
};
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
viewportElement = {
|
||||
addEventListener: () => {},
|
||||
style: {
|
||||
height: 0,
|
||||
lineHeight: 0
|
||||
}
|
||||
};
|
||||
scrollAreaElement = {
|
||||
style: {
|
||||
height: 0
|
||||
}
|
||||
};
|
||||
charMeasure = {
|
||||
height: CHARACTER_HEIGHT
|
||||
};
|
||||
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure);
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should set the height of the viewport when the line-height changed', () => {
|
||||
terminal.buffer.lines.push('');
|
||||
terminal.buffer.lines.push('');
|
||||
terminal.rows = 1;
|
||||
viewport.refresh();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
charMeasure.height = 2 * CHARACTER_HEIGHT;
|
||||
viewport.refresh();
|
||||
assert.equal(viewportElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncScrollArea', () => {
|
||||
it('should sync the scroll area', done => {
|
||||
// Allow CharMeasure to be initialized
|
||||
setTimeout(() => {
|
||||
terminal.buffer.lines.push('');
|
||||
terminal.rows = 1;
|
||||
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
terminal.buffer.lines.push('');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
+16
-21
@@ -12,9 +12,10 @@ import { IColorSet } from './renderer/Interfaces';
|
||||
* Logic for the virtual scroll bar is included in this object.
|
||||
*/
|
||||
export class Viewport implements IViewport {
|
||||
private currentRowHeight: number;
|
||||
private lastRecordedBufferLength: number;
|
||||
private lastRecordedViewportHeight: number;
|
||||
private currentRowHeight: number = 0;
|
||||
private lastRecordedBufferLength: number = 0;
|
||||
private lastRecordedViewportHeight: number = 0;
|
||||
private lastRecordedBufferHeight: number = 0;
|
||||
private lastTouchY: number;
|
||||
|
||||
/**
|
||||
@@ -30,12 +31,6 @@ export class Viewport implements IViewport {
|
||||
private scrollArea: HTMLElement,
|
||||
private charMeasure: CharMeasure
|
||||
) {
|
||||
this.currentRowHeight = 0;
|
||||
this.lastRecordedBufferLength = 0;
|
||||
this.lastRecordedViewportHeight = 0;
|
||||
|
||||
this.terminal.on('scroll', this.syncScrollArea.bind(this));
|
||||
this.terminal.on('resize', this.syncScrollArea.bind(this));
|
||||
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||
|
||||
// Perform this async to ensure the CharMeasure is ready.
|
||||
@@ -52,18 +47,18 @@ export class Viewport implements IViewport {
|
||||
*/
|
||||
private refresh(): void {
|
||||
if (this.charMeasure.height > 0) {
|
||||
const lineHeight = Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight);
|
||||
const rowHeightChanged = lineHeight !== this.currentRowHeight;
|
||||
if (rowHeightChanged) {
|
||||
this.currentRowHeight = lineHeight;
|
||||
this.viewportElement.style.lineHeight = lineHeight + 'px';
|
||||
this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio;
|
||||
|
||||
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
|
||||
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
|
||||
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
|
||||
}
|
||||
const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
|
||||
if (rowHeightChanged || viewportHeightChanged) {
|
||||
this.lastRecordedViewportHeight = this.terminal.rows;
|
||||
this.viewportElement.style.height = lineHeight * this.terminal.rows + 'px';
|
||||
|
||||
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
|
||||
if (this.lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this.lastRecordedBufferHeight = newBufferHeight;
|
||||
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
|
||||
}
|
||||
this.scrollArea.style.height = (lineHeight * this.lastRecordedBufferLength) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +70,12 @@ export class Viewport implements IViewport {
|
||||
// If buffer height changed
|
||||
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
|
||||
this.refresh();
|
||||
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
|
||||
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
|
||||
// If viewport height changed
|
||||
this.refresh();
|
||||
} else {
|
||||
// If size has changed, refresh viewport
|
||||
if (Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight) !== this.currentRowHeight) {
|
||||
if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
var availableWidth = parentElementWidth - elementPaddingHor;
|
||||
var geometry = {
|
||||
cols: Math.floor(availableWidth / term.charMeasure.width),
|
||||
rows: Math.floor(availableHeight / Math.ceil(term.charMeasure.height * term.getOption('lineHeight')))
|
||||
rows: Math.floor(availableHeight / Math.floor(term.charMeasure.height * term.getOption('lineHeight')))
|
||||
};
|
||||
|
||||
return geometry;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
export interface IMouseZoneManager {
|
||||
add(zone: IMouseZone): void;
|
||||
clearAll(): void;
|
||||
clearAll(start?: number, end?: number): void;
|
||||
}
|
||||
|
||||
export interface IMouseZone {
|
||||
|
||||
@@ -44,9 +44,28 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
}
|
||||
|
||||
public clearAll(): void {
|
||||
this._zones.length = 0;
|
||||
this._deactivate();
|
||||
public clearAll(start?: number, end?: number): void {
|
||||
// Exit if there's nothing to clear
|
||||
if (this._zones.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 (this._currentZone && this._currentZone === zone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = null;
|
||||
}
|
||||
this._zones.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivate the mouse zone manager if all the zones have been removed
|
||||
if (this._zones.length === 0) {
|
||||
this._deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
private _activate(): void {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
|
||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||
import { GridCache } from './GridCache';
|
||||
@@ -18,8 +18,8 @@ export class BackgroundRenderLayer extends BaseRenderLayer {
|
||||
this._state = new GridCache<number>();
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state.clear();
|
||||
this._state.resize(terminal.cols, terminal.rows);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderLayer, IColorSet } from './Interfaces';
|
||||
import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { ITerminal, ITerminalOptions } from '../Interfaces';
|
||||
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
|
||||
import { CharData } from '../Types';
|
||||
@@ -61,34 +61,15 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
||||
// Calculate the scaled character dimensions, if devicePixelRatio is a
|
||||
// floating point number then the value is ceiled to ensure there is enough
|
||||
// space to draw the character to the cell
|
||||
this.scaledCharWidth = Math.ceil(terminal.charMeasure.width * window.devicePixelRatio);
|
||||
this.scaledCharHeight = Math.ceil(terminal.charMeasure.height * window.devicePixelRatio);
|
||||
|
||||
// Calculate the scaled line height, if lineHeight is not 1 then the value
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.scaledLineHeight = Math.floor(this.scaledCharHeight * terminal.options.lineHeight);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.scaledLineDrawY = terminal.options.lineHeight === 1 ? 0 : Math.round((this.scaledLineHeight - this.scaledCharHeight) / 2);
|
||||
|
||||
// Recalcualte the canvas dimensions; width/height define the actual number
|
||||
// of pixels in the canvas, style.width/height define the size of the canvas
|
||||
// on the page. It's very important that this rounds to nearest integer and
|
||||
// not ceils as browsers often set window.devicePixelRatio as something like
|
||||
// 1.100000023841858, when it's actually 1.1. Ceiling causes blurriness as
|
||||
// the backing canvas image is 1 pixel too large for the canvas element
|
||||
// size.
|
||||
this._canvas.width = Math.round(canvasWidth * window.devicePixelRatio);
|
||||
this._canvas.height = Math.round(canvasHeight * window.devicePixelRatio);
|
||||
this._canvas.style.width = `${canvasWidth}px`;
|
||||
this._canvas.style.height = `${canvasHeight}px`;
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
this.scaledCharWidth = dim.scaledCharWidth;
|
||||
this.scaledCharHeight = dim.scaledCharHeight;
|
||||
this.scaledLineHeight = dim.scaledLineHeight;
|
||||
this.scaledLineDrawY = dim.scaledLineDrawY;
|
||||
this._canvas.width = dim.scaledCanvasWidth;
|
||||
this._canvas.height = dim.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||
this._canvas.style.height = `${dim.canvasHeight}px`;
|
||||
|
||||
if (charSizeChanged) {
|
||||
this._refreshCharAtlas(terminal, this.colors);
|
||||
@@ -97,6 +78,16 @@ 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.
|
||||
@@ -105,7 +96,12 @@ 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 {
|
||||
this._ctx.fillRect(x * this.scaledCharWidth, y * this.scaledLineHeight, width * this.scaledCharWidth, height * this.scaledLineHeight);
|
||||
const cellLeft = this._getCellLeft(x);
|
||||
this._ctx.fillRect(
|
||||
cellLeft,
|
||||
y * this.scaledLineHeight,
|
||||
this._getCellLeft(x + width) - cellLeft,
|
||||
height * this.scaledLineHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,10 +111,11 @@ 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(
|
||||
x * this.scaledCharWidth,
|
||||
cellLeft,
|
||||
(y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this.scaledCharWidth,
|
||||
this._getCellLeft(x + width) - cellLeft,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
|
||||
@@ -130,7 +127,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected fillLeftLineAtCell(x: number, y: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this.scaledCharWidth,
|
||||
this._getCellLeft(x),
|
||||
y * this.scaledLineHeight,
|
||||
window.devicePixelRatio,
|
||||
this.scaledLineHeight);
|
||||
@@ -143,11 +140,12 @@ 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(
|
||||
x * this.scaledCharWidth + window.devicePixelRatio / 2,
|
||||
cellLeft + window.devicePixelRatio / 2,
|
||||
y * this.scaledLineHeight + (window.devicePixelRatio / 2),
|
||||
(width * this.scaledCharWidth) - window.devicePixelRatio,
|
||||
this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio,
|
||||
(height * this.scaledLineHeight) - window.devicePixelRatio);
|
||||
}
|
||||
|
||||
@@ -166,7 +164,12 @@ 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 {
|
||||
this._ctx.clearRect(x * this.scaledCharWidth, y * this.scaledLineHeight, width * this.scaledCharWidth, height * this.scaledLineHeight);
|
||||
const cellLeft = this._getCellLeft(x);
|
||||
this._ctx.clearRect(
|
||||
cellLeft,
|
||||
y * this.scaledLineHeight,
|
||||
this._getCellLeft(x + width) - cellLeft,
|
||||
height * this.scaledLineHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { GridCache } from './GridCache';
|
||||
@@ -47,8 +47,8 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
x: null,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
||||
import { FLAGS } from './Types';
|
||||
@@ -26,8 +26,8 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
this._state = new GridCache<CharData>();
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state.clear();
|
||||
this._state.resize(terminal.cols, terminal.rows);
|
||||
@@ -159,6 +159,12 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
* @param char The character to search.
|
||||
*/
|
||||
private _isEmoji(char: string): boolean {
|
||||
// TODO: We need a generic solution for handling characters like this
|
||||
// Check special ambiguous width characters
|
||||
if (char === '➜') {
|
||||
return true;
|
||||
}
|
||||
// Check emoji unicode range
|
||||
return char.search(/([\uD800-\uDBFF][\uDC00-\uDFFF])/g) >= 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ITerminalOptions, ITheme } from '../Interfaces';
|
||||
import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces';
|
||||
|
||||
export interface IRenderer extends IEventEmitter {
|
||||
dimensions: IRenderDimensions;
|
||||
|
||||
export interface IRenderer {
|
||||
setTheme(theme: ITheme): IColorSet;
|
||||
onWindowResize(devicePixelRatio: number): void;
|
||||
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
|
||||
@@ -59,7 +61,7 @@ export interface IRenderLayer {
|
||||
/**
|
||||
* Resize the render layer.
|
||||
*/
|
||||
resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void;
|
||||
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
|
||||
|
||||
/**
|
||||
* Clear the state of the render layer.
|
||||
@@ -76,3 +78,14 @@ export interface IColorSet {
|
||||
selection: string;
|
||||
ansi: string[];
|
||||
}
|
||||
|
||||
export interface IRenderDimensions {
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
scaledLineHeight: number;
|
||||
scaledLineDrawY: number;
|
||||
scaledCanvasWidth: number;
|
||||
scaledCanvasHeight: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
|
||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||
import { GridCache } from './GridCache';
|
||||
@@ -20,8 +20,8 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = null;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { ColorManager } from './ColorManager';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { IRenderLayer, IColorSet, IRenderer } from './Interfaces';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
|
||||
export class Renderer implements IRenderer {
|
||||
export class Renderer extends EventEmitter implements IRenderer {
|
||||
/** A queue of the rows to be refreshed */
|
||||
private _refreshRowsQueue: {start: number, end: number}[] = [];
|
||||
private _refreshAnimationFrame = null;
|
||||
@@ -23,8 +24,10 @@ export class Renderer implements IRenderer {
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
private _colorManager: ColorManager;
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
this._colorManager = new ColorManager();
|
||||
this._renderLayers = [
|
||||
new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors),
|
||||
@@ -33,6 +36,16 @@ export class Renderer implements IRenderer {
|
||||
new LinkRenderLayer(this._terminal.element, 3, this._colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.element, 4, this._colorManager.colors)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: null,
|
||||
scaledCharHeight: null,
|
||||
scaledLineHeight: null,
|
||||
scaledLineDrawY: null,
|
||||
scaledCanvasWidth: null,
|
||||
scaledCanvasHeight: null,
|
||||
canvasWidth: null,
|
||||
canvasHeight: null
|
||||
};
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
}
|
||||
|
||||
@@ -63,12 +76,53 @@ export class Renderer implements IRenderer {
|
||||
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
|
||||
return;
|
||||
}
|
||||
const width = this._terminal.charMeasure.width * cols;
|
||||
const height = Math.floor(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * rows;
|
||||
|
||||
// Calculate the scaled character width. Width is kept as a decimal to
|
||||
// provide better letter spacing, otherwise the text can look odd.
|
||||
// Characters drawn using this decimal number do have the potential to
|
||||
// overlap, but only by a single pixel. As such, it's not a big deal when
|
||||
// they do as that pixel is always cleared as necessary before drawing the
|
||||
// character.
|
||||
this.dimensions.scaledCharWidth = this._terminal.charMeasure.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);
|
||||
|
||||
// Calculate the scaled line height, if lineHeight is not 1 then the value
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledLineHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledLineDrawY = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledLineHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight;
|
||||
this.dimensions.scaledCanvasWidth = Math.round(this._terminal.cols * this.dimensions.scaledCharWidth);
|
||||
|
||||
// The the size of the canvas on the page. It's very important that this
|
||||
// rounds to nearest integer and not ceils as browsers often set
|
||||
// window.devicePixelRatio as something like 1.100000023841858, when it's
|
||||
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
|
||||
// pixel too large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
|
||||
|
||||
// Resize all render layers
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, width, height, didCharSizeChange));
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
|
||||
|
||||
// Force a refresh
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
|
||||
this.emit('resize', {
|
||||
width: this.dimensions.canvasWidth,
|
||||
height: this.dimensions.canvasHeight
|
||||
});
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
|
||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||
import { GridCache } from './GridCache';
|
||||
@@ -21,8 +21,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
};
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
start: null,
|
||||
|
||||
@@ -60,7 +60,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
return;
|
||||
}
|
||||
if (this._width !== geometry.width || this._height !== geometry.height) {
|
||||
this._width = Math.ceil(geometry.width);
|
||||
this._width = geometry.width;
|
||||
this._height = Math.ceil(geometry.height);
|
||||
this.emit('charsizechanged');
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user