Merge branch 'v3' into zmodem
This commit is contained in:
@@ -64,7 +64,6 @@ export class CompositionHelper {
|
|||||||
* @param {CompositionEvent} ev The event.
|
* @param {CompositionEvent} ev The event.
|
||||||
*/
|
*/
|
||||||
public compositionupdate(ev: CompositionEvent): void {
|
public compositionupdate(ev: CompositionEvent): void {
|
||||||
console.log('compositionupdate');
|
|
||||||
this.compositionView.textContent = ev.data;
|
this.compositionView.textContent = ev.data;
|
||||||
this.updateCompositionElements();
|
this.updateCompositionElements();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
+3
-1
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { ILinkMatcherOptions } from './Interfaces';
|
import { ILinkMatcherOptions } from './Interfaces';
|
||||||
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
|
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
|
||||||
import { IColorSet } from './renderer/Interfaces';
|
import { IColorSet, IRenderer } from './renderer/Interfaces';
|
||||||
import { IMouseZoneManager } from './input/Interfaces';
|
import { IMouseZoneManager } from './input/Interfaces';
|
||||||
|
|
||||||
export interface IBrowser {
|
export interface IBrowser {
|
||||||
@@ -36,6 +36,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement
|
|||||||
selectionManager: ISelectionManager;
|
selectionManager: ISelectionManager;
|
||||||
charMeasure: ICharMeasure;
|
charMeasure: ICharMeasure;
|
||||||
textarea: HTMLTextAreaElement;
|
textarea: HTMLTextAreaElement;
|
||||||
|
renderer: IRenderer;
|
||||||
rows: number;
|
rows: number;
|
||||||
cols: number;
|
cols: number;
|
||||||
browser: IBrowser;
|
browser: IBrowser;
|
||||||
@@ -323,6 +324,7 @@ export interface ITheme {
|
|||||||
foreground?: string;
|
foreground?: string;
|
||||||
background?: string;
|
background?: string;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
|
cursorAccent?: string;
|
||||||
selection?: string;
|
selection?: string;
|
||||||
black?: string;
|
black?: string;
|
||||||
red?: string;
|
red?: string;
|
||||||
|
|||||||
@@ -156,6 +156,17 @@ describe('Linkifier', () => {
|
|||||||
linkifier.linkifyRows();
|
linkifier.linkifyRows();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should validate the uri, not the row', done => {
|
||||||
|
addRow('abc test abc');
|
||||||
|
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||||
|
validationCallback: (uri, cb) => {
|
||||||
|
assert.equal(uri, 'test');
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
linkifier.linkifyRows();
|
||||||
|
});
|
||||||
|
|
||||||
it('should disable link if false', done => {
|
it('should disable link if false', done => {
|
||||||
addRow('test');
|
addRow('test');
|
||||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||||
|
|||||||
+24
-9
@@ -49,11 +49,16 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
private _mouseZoneManager: IMouseZoneManager;
|
private _mouseZoneManager: IMouseZoneManager;
|
||||||
private _rowsTimeoutId: number;
|
private _rowsTimeoutId: number;
|
||||||
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||||
|
private _rowsToLinkify: {start: number, end: number};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected _terminal: IBufferAccessor & IElementAccessor
|
protected _terminal: IBufferAccessor & IElementAccessor
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this._rowsToLinkify = {
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
};
|
||||||
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,25 +81,35 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out any existing links
|
// Increase range to linkify
|
||||||
this._mouseZoneManager.clearAll();
|
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) {
|
if (this._rowsTimeoutId) {
|
||||||
clearTimeout(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
|
* Linkifies the rows requested.
|
||||||
* @param start The row to start at.
|
|
||||||
* @param end The row to end at.
|
|
||||||
*/
|
*/
|
||||||
private _linkifyRows(start: number, end: number): void {
|
private _linkifyRows(): void {
|
||||||
this._rowsTimeoutId = null;
|
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._linkifyRow(i);
|
||||||
}
|
}
|
||||||
|
this._rowsToLinkify.start = null;
|
||||||
|
this._rowsToLinkify.end = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,7 +236,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
|
|
||||||
// Ensure the link is valid before registering
|
// Ensure the link is valid before registering
|
||||||
if (matcher.validationCallback) {
|
if (matcher.validationCallback) {
|
||||||
matcher.validationCallback(text, isValid => {
|
matcher.validationCallback(uri, isValid => {
|
||||||
// Discard link if the line has already changed
|
// Discard link if the line has already changed
|
||||||
if (this._rowsTimeoutId) {
|
if (this._rowsTimeoutId) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -454,6 +454,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* @param event The mousemove event.
|
* @param event The mousemove event.
|
||||||
*/
|
*/
|
||||||
private _onMouseMove(event: MouseEvent): void {
|
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
|
// Record the previous position so we know whether to redraw the selection
|
||||||
// at the end.
|
// at the end.
|
||||||
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
|
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
|
||||||
|
|||||||
+26
-16
@@ -48,8 +48,11 @@ import { MouseZoneManager } from './input/MouseZoneManager';
|
|||||||
import { initialize as initializeCharAtlas } from './renderer/CharAtlas';
|
import { initialize as initializeCharAtlas } from './renderer/CharAtlas';
|
||||||
import { IRenderer } from './renderer/Interfaces';
|
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 define: any;
|
||||||
|
declare var require: any;
|
||||||
|
|
||||||
// Let it work inside Node.js for automated testing purposes.
|
// Let it work inside Node.js for automated testing purposes.
|
||||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||||
@@ -188,7 +191,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
|||||||
|
|
||||||
private inputHandler: InputHandler;
|
private inputHandler: InputHandler;
|
||||||
private parser: Parser;
|
private parser: Parser;
|
||||||
private renderer: IRenderer;
|
public renderer: IRenderer;
|
||||||
public selectionManager: SelectionManager;
|
public selectionManager: SelectionManager;
|
||||||
public linkifier: ILinkifier;
|
public linkifier: ILinkifier;
|
||||||
public buffers: BufferSet;
|
public buffers: BufferSet;
|
||||||
@@ -588,6 +591,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
|||||||
this.syncBellSound();
|
this.syncBellSound();
|
||||||
|
|
||||||
this._mouseZoneManager = new MouseZoneManager(this);
|
this._mouseZoneManager = new MouseZoneManager(this);
|
||||||
|
this.on('scroll', () => this._mouseZoneManager.clearAll());
|
||||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||||
|
|
||||||
// Create the container that will hold helpers like the textarea for
|
// Create the container that will hold helpers like the textarea for
|
||||||
@@ -618,15 +622,18 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
|||||||
|
|
||||||
this.charMeasure = new CharMeasure(document, this.helperContainer);
|
this.charMeasure = new CharMeasure(document, this.helperContainer);
|
||||||
|
|
||||||
|
this.renderer = new Renderer(this, this.options.theme);
|
||||||
|
this.options.theme = null;
|
||||||
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
|
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
|
||||||
this.charMeasure.on('charsizechanged', () => this.viewport.syncScrollArea());
|
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
|
||||||
this.renderer = new Renderer(this);
|
|
||||||
this.on('cursormove', () => this.renderer.onCursorMove());
|
this.on('cursormove', () => this.renderer.onCursorMove());
|
||||||
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
|
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
|
||||||
this.on('blur', () => this.renderer.onBlur());
|
this.on('blur', () => this.renderer.onBlur());
|
||||||
this.on('focus', () => this.renderer.onFocus());
|
this.on('focus', () => this.renderer.onFocus());
|
||||||
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
|
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
|
||||||
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
|
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.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure);
|
||||||
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
|
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
|
||||||
@@ -639,21 +646,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
|||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
this.textarea.select();
|
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());
|
this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
|
||||||
|
|
||||||
// Measure the character size
|
// Measure the character size
|
||||||
this.charMeasure.measure(this.options);
|
this.charMeasure.measure(this.options);
|
||||||
|
|
||||||
// Set the theme if it was set via setOption/constructor before open. This
|
|
||||||
// must be run after CharMeasure.measure as it depends on char dimensions.
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.options.theme) {
|
|
||||||
this._setTheme(this.options.theme);
|
|
||||||
this.options.theme = null;
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Setup loop that draws to screen
|
// Setup loop that draws to screen
|
||||||
this.refresh(0, this.rows - 1);
|
this.refresh(0, this.rows - 1);
|
||||||
|
|
||||||
@@ -1034,7 +1035,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
|||||||
*/
|
*/
|
||||||
private queueLinkification(start: number, end: number): void {
|
private queueLinkification(start: number, end: number): void {
|
||||||
if (this.linkifier) {
|
if (this.linkifier) {
|
||||||
this.linkifier.linkifyRows(0, this.rows);
|
this.linkifier.linkifyRows(start, end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,7 +1072,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
|||||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||||
if (!willBufferBeTrimmed) {
|
if (!willBufferBeTrimmed) {
|
||||||
this.buffer.ybase++;
|
this.buffer.ybase++;
|
||||||
this.buffer.ydisp++;
|
// Only scroll the ydisp with ybase if the user has not scrolled up
|
||||||
|
if (!this.userScrolling) {
|
||||||
|
this.buffer.ydisp++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// When the buffer is full and the user has scrolled up, keep the text
|
||||||
|
// stable unless ydisp is right at the top
|
||||||
|
if (this.userScrolling) {
|
||||||
|
this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// scrollTop is non-zero which means no line will be going to the
|
// scrollTop is non-zero which means no line will be going to the
|
||||||
|
|||||||
@@ -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.
|
* Logic for the virtual scroll bar is included in this object.
|
||||||
*/
|
*/
|
||||||
export class Viewport implements IViewport {
|
export class Viewport implements IViewport {
|
||||||
private currentRowHeight: number;
|
private currentRowHeight: number = 0;
|
||||||
private lastRecordedBufferLength: number;
|
private lastRecordedBufferLength: number = 0;
|
||||||
private lastRecordedViewportHeight: number;
|
private lastRecordedViewportHeight: number = 0;
|
||||||
|
private lastRecordedBufferHeight: number = 0;
|
||||||
private lastTouchY: number;
|
private lastTouchY: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,12 +31,6 @@ export class Viewport implements IViewport {
|
|||||||
private scrollArea: HTMLElement,
|
private scrollArea: HTMLElement,
|
||||||
private charMeasure: CharMeasure
|
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));
|
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||||
|
|
||||||
// Perform this async to ensure the CharMeasure is ready.
|
// Perform this async to ensure the CharMeasure is ready.
|
||||||
@@ -52,18 +47,18 @@ export class Viewport implements IViewport {
|
|||||||
*/
|
*/
|
||||||
private refresh(): void {
|
private refresh(): void {
|
||||||
if (this.charMeasure.height > 0) {
|
if (this.charMeasure.height > 0) {
|
||||||
const lineHeight = Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight);
|
this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio;
|
||||||
const rowHeightChanged = lineHeight !== this.currentRowHeight;
|
|
||||||
if (rowHeightChanged) {
|
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
|
||||||
this.currentRowHeight = lineHeight;
|
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
|
||||||
this.viewportElement.style.lineHeight = lineHeight + 'px';
|
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
|
||||||
}
|
}
|
||||||
const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
|
|
||||||
if (rowHeightChanged || viewportHeightChanged) {
|
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
|
||||||
this.lastRecordedViewportHeight = this.terminal.rows;
|
if (this.lastRecordedBufferHeight !== newBufferHeight) {
|
||||||
this.viewportElement.style.height = lineHeight * this.terminal.rows + 'px';
|
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
|
// If buffer height changed
|
||||||
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
|
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
|
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
|
||||||
// If viewport height changed
|
// If viewport height changed
|
||||||
this.refresh();
|
this.refresh();
|
||||||
} else {
|
} else {
|
||||||
// If size has changed, refresh viewport
|
// 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();
|
this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
var availableWidth = parentElementWidth - elementPaddingHor;
|
var availableWidth = parentElementWidth - elementPaddingHor;
|
||||||
var geometry = {
|
var geometry = {
|
||||||
cols: Math.floor(availableWidth / term.charMeasure.width),
|
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;
|
return geometry;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
export interface IMouseZoneManager {
|
export interface IMouseZoneManager {
|
||||||
add(zone: IMouseZone): void;
|
add(zone: IMouseZone): void;
|
||||||
clearAll(): void;
|
clearAll(start?: number, end?: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMouseZone {
|
export interface IMouseZone {
|
||||||
|
|||||||
@@ -44,9 +44,28 @@ export class MouseZoneManager implements IMouseZoneManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearAll(): void {
|
public clearAll(start?: number, end?: number): void {
|
||||||
this._zones.length = 0;
|
// Exit if there's nothing to clear
|
||||||
this._deactivate();
|
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 {
|
private _activate(): void {
|
||||||
@@ -83,10 +102,14 @@ export class MouseZoneManager implements IMouseZoneManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire the hover end callback if a zone was being hovered
|
// Fire the hover end callback and cancel any existing timer if a new zone
|
||||||
|
// is being hovered
|
||||||
if (this._currentZone) {
|
if (this._currentZone) {
|
||||||
this._currentZone.leaveCallback();
|
this._currentZone.leaveCallback();
|
||||||
this._currentZone = null;
|
this._currentZone = null;
|
||||||
|
if (this._tooltipTimeout) {
|
||||||
|
clearTimeout(this._tooltipTimeout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if there is not zone
|
// Exit if there is not zone
|
||||||
@@ -100,14 +123,12 @@ export class MouseZoneManager implements IMouseZoneManager {
|
|||||||
zone.hoverCallback(e);
|
zone.hoverCallback(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart the timeout
|
// Restart the tooltip timeout
|
||||||
if (this._tooltipTimeout) {
|
|
||||||
clearTimeout(this._tooltipTimeout);
|
|
||||||
}
|
|
||||||
this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
|
this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onTooltip(e: MouseEvent): void {
|
private _onTooltip(e: MouseEvent): void {
|
||||||
|
this._tooltipTimeout = null;
|
||||||
const zone = this._findZoneEventAt(e);
|
const zone = this._findZoneEventAt(e);
|
||||||
if (zone && zone.tooltipCallback) {
|
if (zone && zone.tooltipCallback) {
|
||||||
zone.tooltipCallback(e);
|
zone.tooltipCallback(e);
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IColorSet } from './Interfaces';
|
|
||||||
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
|
|
||||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
|
||||||
import { GridCache } from './GridCache';
|
|
||||||
import { FLAGS } from './Types';
|
|
||||||
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
|
|
||||||
|
|
||||||
export class BackgroundRenderLayer extends BaseRenderLayer {
|
|
||||||
private _state: GridCache<number>;
|
|
||||||
|
|
||||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
|
||||||
super(container, 'bg', zIndex, colors);
|
|
||||||
this._state = new GridCache<number>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
|
||||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
|
||||||
// Resizing the canvas discards the contents of the canvas so clear state
|
|
||||||
this._state.clear();
|
|
||||||
this._state.resize(terminal.cols, terminal.rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset(terminal: ITerminal): void {
|
|
||||||
this._state.clear();
|
|
||||||
this.clearAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
|
|
||||||
// Resize has not been called yet
|
|
||||||
if (this._state.cache.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let y = startRow; y <= endRow; y++) {
|
|
||||||
let row = y + terminal.buffer.ydisp;
|
|
||||||
let line = terminal.buffer.lines.get(row);
|
|
||||||
for (let x = 0; x < terminal.cols; x++) {
|
|
||||||
const attr: number = line[x][CHAR_DATA_ATTR_INDEX];
|
|
||||||
let bg = attr & 0x1ff;
|
|
||||||
const flags = attr >> 18;
|
|
||||||
|
|
||||||
// If inverse flag is on, the background should become the foreground.
|
|
||||||
if (flags & FLAGS.INVERSE) {
|
|
||||||
bg = (attr >> 9) & 0x1ff;
|
|
||||||
if (bg === 257) {
|
|
||||||
bg = INVERTED_DEFAULT_COLOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellState = this._state.cache[x][y];
|
|
||||||
const needsRefresh = (bg < 256 && cellState !== bg) || cellState !== null;
|
|
||||||
if (needsRefresh) {
|
|
||||||
if (bg < 256) {
|
|
||||||
this._ctx.save();
|
|
||||||
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this.colors.foreground : this.colors.ansi[bg]);
|
|
||||||
this.fillCells(x, y, 1, 1);
|
|
||||||
this._ctx.restore();
|
|
||||||
this._state.cache[x][y] = bg;
|
|
||||||
} else {
|
|
||||||
this.clearCells(x, y, 1, 1);
|
|
||||||
this._state.cache[x][y] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+100
-64
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRenderLayer, IColorSet } from './Interfaces';
|
import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
|
||||||
import { ITerminal, ITerminalOptions } from '../Interfaces';
|
import { ITerminal, ITerminalOptions } from '../Interfaces';
|
||||||
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
|
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
|
||||||
import { CharData } from '../Types';
|
import { CharData } from '../Types';
|
||||||
@@ -14,10 +14,10 @@ export const INVERTED_DEFAULT_COLOR = -1;
|
|||||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||||
private _canvas: HTMLCanvasElement;
|
private _canvas: HTMLCanvasElement;
|
||||||
protected _ctx: CanvasRenderingContext2D;
|
protected _ctx: CanvasRenderingContext2D;
|
||||||
private scaledCharWidth: number;
|
private _scaledCharWidth: number;
|
||||||
private scaledCharHeight: number;
|
private _scaledCharHeight: number;
|
||||||
private scaledLineHeight: number;
|
private _scaledLineHeight: number;
|
||||||
private scaledLineDrawY: number;
|
private _scaledLineDrawY: number;
|
||||||
|
|
||||||
private _charAtlas: HTMLCanvasElement | ImageBitmap;
|
private _charAtlas: HTMLCanvasElement | ImageBitmap;
|
||||||
|
|
||||||
@@ -25,13 +25,18 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
id: string,
|
id: string,
|
||||||
zIndex: number,
|
zIndex: number,
|
||||||
protected colors: IColorSet
|
private _alpha: boolean,
|
||||||
|
protected _colors: IColorSet
|
||||||
) {
|
) {
|
||||||
this._canvas = document.createElement('canvas');
|
this._canvas = document.createElement('canvas');
|
||||||
this._canvas.id = `xterm-${id}-layer`;
|
this._canvas.id = `xterm-${id}-layer`;
|
||||||
this._canvas.style.zIndex = zIndex.toString();
|
this._canvas.style.zIndex = zIndex.toString();
|
||||||
this._ctx = this._canvas.getContext('2d');
|
this._ctx = this._canvas.getContext('2d', {_alpha});
|
||||||
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||||
|
// Draw the background if this is an opaque layer
|
||||||
|
if (!_alpha) {
|
||||||
|
this.clearAll();
|
||||||
|
}
|
||||||
container.appendChild(this._canvas);
|
container.appendChild(this._canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +57,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param colorSet The color set to use for the char atlas.
|
* @param colorSet The color set to use for the char atlas.
|
||||||
*/
|
*/
|
||||||
private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void {
|
private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void {
|
||||||
|
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._charAtlas = null;
|
this._charAtlas = null;
|
||||||
const result = acquireCharAtlas(terminal, this.colors, this.scaledCharWidth, this.scaledCharHeight);
|
const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
|
||||||
if (result instanceof HTMLCanvasElement) {
|
if (result instanceof HTMLCanvasElement) {
|
||||||
this._charAtlas = result;
|
this._charAtlas = result;
|
||||||
} else {
|
} else {
|
||||||
@@ -61,42 +69,38 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||||
// Calculate the scaled character dimensions, if devicePixelRatio is a
|
this._scaledCharWidth = dim.scaledCharWidth;
|
||||||
// floating point number then the value is ceiled to ensure there is enough
|
this._scaledCharHeight = dim.scaledCharHeight;
|
||||||
// space to draw the character to the cell
|
this._scaledLineHeight = dim.scaledLineHeight;
|
||||||
this.scaledCharWidth = Math.ceil(terminal.charMeasure.width * window.devicePixelRatio);
|
this._scaledLineDrawY = dim.scaledLineDrawY;
|
||||||
this.scaledCharHeight = Math.ceil(terminal.charMeasure.height * window.devicePixelRatio);
|
this._canvas.width = dim.scaledCanvasWidth;
|
||||||
|
this._canvas.height = dim.scaledCanvasHeight;
|
||||||
|
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||||
|
this._canvas.style.height = `${dim.canvasHeight}px`;
|
||||||
|
|
||||||
// Calculate the scaled line height, if lineHeight is not 1 then the value
|
// Draw the background if this is an opaque layer
|
||||||
// will be floored because since lineHeight can never be lower then 1, there
|
if (!this._alpha) {
|
||||||
// is a guarentee that the scaled line height will always be larger than
|
this.clearAll();
|
||||||
// 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`;
|
|
||||||
|
|
||||||
if (charSizeChanged) {
|
if (charSizeChanged) {
|
||||||
this._refreshCharAtlas(terminal, this.colors);
|
this._refreshCharAtlas(terminal, this._colors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract reset(terminal: ITerminal): void;
|
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.
|
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
|
||||||
* @param x The column to start at.
|
* @param x The column to start at.
|
||||||
@@ -105,7 +109,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param height The number of rows to fill.
|
* @param height The number of rows to fill.
|
||||||
*/
|
*/
|
||||||
protected fillCells(x: number, y: number, width: number, height: number): void {
|
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 +124,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param y The row to fill.
|
* @param y The row to fill.
|
||||||
*/
|
*/
|
||||||
protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||||
|
const cellLeft = this._getCellLeft(x);
|
||||||
this._ctx.fillRect(
|
this._ctx.fillRect(
|
||||||
x * this.scaledCharWidth,
|
cellLeft,
|
||||||
(y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
(y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||||
width * this.scaledCharWidth,
|
this._getCellLeft(x + width) - cellLeft,
|
||||||
window.devicePixelRatio);
|
window.devicePixelRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,10 +140,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
*/
|
*/
|
||||||
protected fillLeftLineAtCell(x: number, y: number): void {
|
protected fillLeftLineAtCell(x: number, y: number): void {
|
||||||
this._ctx.fillRect(
|
this._ctx.fillRect(
|
||||||
x * this.scaledCharWidth,
|
this._getCellLeft(x),
|
||||||
y * this.scaledLineHeight,
|
y * this._scaledLineHeight,
|
||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
this.scaledLineHeight);
|
this._scaledLineHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,19 +153,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param y The row to fill.
|
* @param y The row to fill.
|
||||||
*/
|
*/
|
||||||
protected strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
protected strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||||
|
const cellLeft = this._getCellLeft(x);
|
||||||
this._ctx.lineWidth = window.devicePixelRatio;
|
this._ctx.lineWidth = window.devicePixelRatio;
|
||||||
this._ctx.strokeRect(
|
this._ctx.strokeRect(
|
||||||
x * this.scaledCharWidth + window.devicePixelRatio / 2,
|
cellLeft + window.devicePixelRatio / 2,
|
||||||
y * this.scaledLineHeight + (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);
|
(height * this._scaledLineHeight) - window.devicePixelRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the entire canvas.
|
* Clears the entire canvas.
|
||||||
*/
|
*/
|
||||||
protected clearAll(): void {
|
protected clearAll(): void {
|
||||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
if (this._alpha) {
|
||||||
|
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||||
|
} else {
|
||||||
|
this._ctx.fillStyle = this._colors.background;
|
||||||
|
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +182,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param height The number of rows to clear.
|
* @param height The number of rows to clear.
|
||||||
*/
|
*/
|
||||||
protected clearCells(x: number, y: number, width: number, height: number): void {
|
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);
|
||||||
|
if (this._alpha) {
|
||||||
|
this._ctx.clearRect(
|
||||||
|
cellLeft,
|
||||||
|
y * this._scaledLineHeight,
|
||||||
|
this._getCellLeft(x + width) - cellLeft,
|
||||||
|
height * this._scaledLineHeight);
|
||||||
|
} else {
|
||||||
|
this._ctx.fillStyle = this._colors.background;
|
||||||
|
this._ctx.fillRect(
|
||||||
|
cellLeft,
|
||||||
|
y * this._scaledLineHeight,
|
||||||
|
this._getCellLeft(x + width) - cellLeft,
|
||||||
|
height * this._scaledLineHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,9 +218,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
// can bleed into other cells. This code will clip the following fillText,
|
// can bleed into other cells. This code will clip the following fillText,
|
||||||
// ensuring that its contents don't go beyond the cell bounds.
|
// ensuring that its contents don't go beyond the cell bounds.
|
||||||
this._ctx.beginPath();
|
this._ctx.beginPath();
|
||||||
this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this.scaledCharWidth, this.scaledCharHeight);
|
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, this._scaledCharHeight);
|
||||||
this._ctx.clip();
|
this._ctx.clip();
|
||||||
this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this.scaledCharWidth, y * this.scaledCharHeight);
|
this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCharWidth, y * this._scaledCharHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,9 +233,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param x The column to draw at.
|
* @param x The column to draw at.
|
||||||
* @param y The row to draw at.
|
* @param y The row to draw at.
|
||||||
* @param fg The foreground color, in the format stored within the attributes.
|
* @param fg The foreground color, in the format stored within the attributes.
|
||||||
|
* @param bg The background color, in the format stored within the attributes.
|
||||||
|
* This is used to validate whether a cached image can be used.
|
||||||
* @param bold Whether the text is bold.
|
* @param bold Whether the text is bold.
|
||||||
*/
|
*/
|
||||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bold: boolean): void {
|
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean): void {
|
||||||
// Clear the cell next to this character if it's wide
|
// Clear the cell next to this character if it's wide
|
||||||
if (width === 2) {
|
if (width === 2) {
|
||||||
this.clearCells(x + 1, y, 1, 1);
|
this.clearCells(x + 1, y, 1, 1);
|
||||||
@@ -223,15 +255,16 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
const isAscii = code < 256;
|
const isAscii = code < 256;
|
||||||
const isBasicColor = (colorIndex > 1 && fg < 16);
|
const isBasicColor = (colorIndex > 1 && fg < 16);
|
||||||
const isDefaultColor = fg >= 256;
|
const isDefaultColor = fg >= 256;
|
||||||
if (isAscii && (isBasicColor || isDefaultColor)) {
|
const isDefaultBackground = bg >= 256;
|
||||||
|
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
|
||||||
// ImageBitmap's draw about twice as fast as from a canvas
|
// ImageBitmap's draw about twice as fast as from a canvas
|
||||||
const charAtlasCellWidth = this.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
||||||
const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
||||||
this._ctx.drawImage(this._charAtlas,
|
this._ctx.drawImage(this._charAtlas,
|
||||||
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight,
|
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this._scaledCharWidth, this._scaledCharHeight,
|
||||||
x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, this.scaledCharWidth, this.scaledCharHeight);
|
x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight);
|
||||||
} else {
|
} else {
|
||||||
this._drawUncachedChar(terminal, char, width, fg, x, y);
|
this._drawUncachedChar(terminal, char, width, fg, x, y, bold);
|
||||||
}
|
}
|
||||||
// This draws the atlas (for debugging purposes)
|
// This draws the atlas (for debugging purposes)
|
||||||
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||||
@@ -249,18 +282,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param x The column to draw at.
|
* @param x The column to draw at.
|
||||||
* @param y The row to draw at.
|
* @param y The row to draw at.
|
||||||
*/
|
*/
|
||||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number): void {
|
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean): void {
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||||
|
if (bold) {
|
||||||
|
this._ctx.font = `bold ${this._ctx.font}`;
|
||||||
|
}
|
||||||
this._ctx.textBaseline = 'top';
|
this._ctx.textBaseline = 'top';
|
||||||
|
|
||||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||||
this._ctx.fillStyle = this.colors.background;
|
this._ctx.fillStyle = this._colors.background;
|
||||||
} else if (fg < 256) {
|
} else if (fg < 256) {
|
||||||
// 256 color support
|
// 256 color support
|
||||||
this._ctx.fillStyle = this.colors.ansi[fg];
|
this._ctx.fillStyle = this._colors.ansi[fg];
|
||||||
} else {
|
} else {
|
||||||
this._ctx.fillStyle = this.colors.foreground;
|
this._ctx.fillStyle = this._colors.foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since uncached characters are not coming off the char atlas with source
|
// Since uncached characters are not coming off the char atlas with source
|
||||||
@@ -268,11 +304,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
// can bleed into other cells. This code will clip the following fillText,
|
// can bleed into other cells. This code will clip the following fillText,
|
||||||
// ensuring that its contents don't go beyond the cell bounds.
|
// ensuring that its contents don't go beyond the cell bounds.
|
||||||
this._ctx.beginPath();
|
this._ctx.beginPath();
|
||||||
this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, width * this.scaledCharWidth, this.scaledCharHeight);
|
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, width * this._scaledCharWidth, this._scaledCharHeight);
|
||||||
this._ctx.clip();
|
this._ctx.clip();
|
||||||
|
|
||||||
// Draw the character
|
// Draw the character
|
||||||
this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY);
|
this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newEntry: ICharAtlasCacheEntry = {
|
const newEntry: ICharAtlasCacheEntry = {
|
||||||
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.foreground, colors.ansi),
|
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi),
|
||||||
config: newConfig,
|
config: newConfig,
|
||||||
ownedBy: [terminal]
|
ownedBy: [terminal]
|
||||||
};
|
};
|
||||||
@@ -75,8 +75,9 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
|
|||||||
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
|
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
|
||||||
const clonedColors = {
|
const clonedColors = {
|
||||||
foreground: colors.foreground,
|
foreground: colors.foreground,
|
||||||
background: null,
|
background: colors.background,
|
||||||
cursor: null,
|
cursor: null,
|
||||||
|
cursorAccent: null,
|
||||||
selection: null,
|
selection: null,
|
||||||
ansi: colors.ansi.slice(0, 16)
|
ansi: colors.ansi.slice(0, 16)
|
||||||
};
|
};
|
||||||
@@ -99,7 +100,8 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
|||||||
a.fontSize === b.fontSize &&
|
a.fontSize === b.fontSize &&
|
||||||
a.scaledCharWidth === b.scaledCharWidth &&
|
a.scaledCharWidth === b.scaledCharWidth &&
|
||||||
a.scaledCharHeight === b.scaledCharHeight &&
|
a.scaledCharHeight === b.scaledCharHeight &&
|
||||||
a.colors.foreground === b.colors.foreground;
|
a.colors.foreground === b.colors.foreground &&
|
||||||
|
a.colors.background === b.colors.background;
|
||||||
}
|
}
|
||||||
|
|
||||||
let generator: CharAtlasGenerator;
|
let generator: CharAtlasGenerator;
|
||||||
@@ -120,16 +122,19 @@ class CharAtlasGenerator {
|
|||||||
|
|
||||||
constructor(private _document: Document) {
|
constructor(private _document: Document) {
|
||||||
this._canvas = this._document.createElement('canvas');
|
this._canvas = this._document.createElement('canvas');
|
||||||
this._ctx = this._canvas.getContext('2d');
|
this._ctx = this._canvas.getContext('2d', {alpha: false});
|
||||||
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
|
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
|
||||||
const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
||||||
const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
||||||
this._canvas.width = 255 * cellWidth;
|
this._canvas.width = 255 * cellWidth;
|
||||||
this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight;
|
this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight;
|
||||||
|
|
||||||
|
this._ctx.fillStyle = background;
|
||||||
|
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||||
|
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.fillStyle = foreground;
|
this._ctx.fillStyle = foreground;
|
||||||
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
|
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet } from './Interfaces';
|
import { IColorSet, IColorManager } from './Interfaces';
|
||||||
import { ITheme } from '../Interfaces';
|
import { ITheme } from '../Interfaces';
|
||||||
|
|
||||||
const DEFAULT_FOREGROUND = '#ffffff';
|
const DEFAULT_FOREGROUND = '#ffffff';
|
||||||
const DEFAULT_BACKGROUND = '#000000';
|
const DEFAULT_BACKGROUND = '#000000';
|
||||||
const DEFAULT_CURSOR = '#ffffff';
|
const DEFAULT_CURSOR = '#ffffff';
|
||||||
|
const DEFAULT_CURSOR_ACCENT = '#000000';
|
||||||
const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)';
|
const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)';
|
||||||
export const DEFAULT_ANSI_COLORS = [
|
export const DEFAULT_ANSI_COLORS = [
|
||||||
// dark:
|
// dark:
|
||||||
@@ -64,7 +65,7 @@ function toPaddedHex(c: number): string {
|
|||||||
/**
|
/**
|
||||||
* Manages the source of truth for a terminal's colors.
|
* Manages the source of truth for a terminal's colors.
|
||||||
*/
|
*/
|
||||||
export class ColorManager {
|
export class ColorManager implements IColorManager {
|
||||||
public colors: IColorSet;
|
public colors: IColorSet;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -72,6 +73,7 @@ export class ColorManager {
|
|||||||
foreground: DEFAULT_FOREGROUND,
|
foreground: DEFAULT_FOREGROUND,
|
||||||
background: DEFAULT_BACKGROUND,
|
background: DEFAULT_BACKGROUND,
|
||||||
cursor: DEFAULT_CURSOR,
|
cursor: DEFAULT_CURSOR,
|
||||||
|
cursorAccent: DEFAULT_CURSOR_ACCENT,
|
||||||
selection: DEFAULT_SELECTION,
|
selection: DEFAULT_SELECTION,
|
||||||
ansi: generate256Colors(DEFAULT_ANSI_COLORS)
|
ansi: generate256Colors(DEFAULT_ANSI_COLORS)
|
||||||
};
|
};
|
||||||
@@ -86,6 +88,7 @@ export class ColorManager {
|
|||||||
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
|
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
|
||||||
this.colors.background = theme.background || DEFAULT_BACKGROUND;
|
this.colors.background = theme.background || DEFAULT_BACKGROUND;
|
||||||
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
|
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
|
||||||
|
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
|
||||||
this.colors.selection = theme.selection || DEFAULT_SELECTION;
|
this.colors.selection = theme.selection || DEFAULT_SELECTION;
|
||||||
this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0];
|
this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0];
|
||||||
this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1];
|
this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1];
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet } from './Interfaces';
|
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||||
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } 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 { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||||
import { GridCache } from './GridCache';
|
import { GridCache } from './GridCache';
|
||||||
@@ -31,7 +31,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
private _isFocused: boolean;
|
private _isFocused: boolean;
|
||||||
|
|
||||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||||
super(container, 'cursor', zIndex, colors);
|
super(container, 'cursor', zIndex, true, colors);
|
||||||
this._state = {
|
this._state = {
|
||||||
x: null,
|
x: null,
|
||||||
y: null,
|
y: null,
|
||||||
@@ -47,8 +47,8 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
|
// 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 {
|
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
super.resize(terminal, dim, charSizeChanged);
|
||||||
// Resizing the canvas discards the contents of the canvas so clear state
|
// Resizing the canvas discards the contents of the canvas so clear state
|
||||||
this._state = {
|
this._state = {
|
||||||
x: null,
|
x: null,
|
||||||
@@ -135,7 +135,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
if (!terminal.isFocused) {
|
if (!terminal.isFocused) {
|
||||||
this._clearCursor();
|
this._clearCursor();
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.fillStyle = this.colors.cursor;
|
this._ctx.fillStyle = this._colors.cursor;
|
||||||
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
this._state.x = terminal.buffer.x;
|
this._state.x = terminal.buffer.x;
|
||||||
@@ -190,30 +190,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
|
|
||||||
private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.fillStyle = this.colors.cursor;
|
this._ctx.fillStyle = this._colors.cursor;
|
||||||
this.fillLeftLineAtCell(x, y);
|
this.fillLeftLineAtCell(x, y);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.fillStyle = this.colors.cursor;
|
this._ctx.fillStyle = this._colors.cursor;
|
||||||
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
||||||
this._ctx.fillStyle = this.colors.background;
|
this._ctx.fillStyle = this._colors.cursorAccent;
|
||||||
this.fillCharTrueColor(terminal, charData, x, y);
|
this.fillCharTrueColor(terminal, charData, x, y);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.fillStyle = this.colors.cursor;
|
this._ctx.fillStyle = this._colors.cursor;
|
||||||
this.fillBottomLineAtCells(x, y);
|
this.fillBottomLineAtCells(x, y);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.strokeStyle = this.colors.cursor;
|
this._ctx.strokeStyle = this._colors.cursor;
|
||||||
this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal, ITerminalOptions, ITheme } from '../Interfaces';
|
import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces';
|
||||||
|
|
||||||
|
export interface IRenderer extends IEventEmitter {
|
||||||
|
dimensions: IRenderDimensions;
|
||||||
|
colorManager: IColorManager;
|
||||||
|
|
||||||
export interface IRenderer {
|
|
||||||
setTheme(theme: ITheme): IColorSet;
|
setTheme(theme: ITheme): IColorSet;
|
||||||
onWindowResize(devicePixelRatio: number): void;
|
onWindowResize(devicePixelRatio: number): void;
|
||||||
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
|
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
|
||||||
@@ -59,7 +62,7 @@ export interface IRenderLayer {
|
|||||||
/**
|
/**
|
||||||
* Resize the render layer.
|
* 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.
|
* Clear the state of the render layer.
|
||||||
@@ -67,11 +70,26 @@ export interface IRenderLayer {
|
|||||||
reset(terminal: ITerminal): void;
|
reset(terminal: ITerminal): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IColorManager {
|
||||||
|
colors: IColorSet;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IColorSet {
|
export interface IColorSet {
|
||||||
foreground: string;
|
foreground: string;
|
||||||
background: string;
|
background: string;
|
||||||
cursor: string;
|
cursor: string;
|
||||||
|
cursorAccent: string;
|
||||||
selection: string;
|
selection: string;
|
||||||
ansi: 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
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet } from './Interfaces';
|
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||||
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
|
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
|
||||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||||
import { GridCache } from './GridCache';
|
import { GridCache } from './GridCache';
|
||||||
@@ -15,13 +15,13 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
|||||||
private _state: LinkHoverEvent = null;
|
private _state: LinkHoverEvent = null;
|
||||||
|
|
||||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
||||||
super(container, 'link', zIndex, colors);
|
super(container, 'link', zIndex, true, colors);
|
||||||
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
|
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
|
||||||
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
|
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
super.resize(terminal, dim, charSizeChanged);
|
||||||
// Resizing the canvas discards the contents of the canvas so clear state
|
// Resizing the canvas discards the contents of the canvas so clear state
|
||||||
this._state = null;
|
this._state = null;
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onLinkHover(e: LinkHoverEvent): void {
|
private _onLinkHover(e: LinkHoverEvent): void {
|
||||||
this._ctx.fillStyle = this.colors.foreground;
|
this._ctx.fillStyle = this._colors.foreground;
|
||||||
this.fillBottomLineAtCells(e.x, e.y, e.length);
|
this.fillBottomLineAtCells(e.x, e.y, e.length);
|
||||||
this._state = e;
|
this._state = e;
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-18
@@ -5,16 +5,16 @@
|
|||||||
|
|
||||||
import { ITerminal, ITheme } from '../Interfaces';
|
import { ITerminal, ITheme } from '../Interfaces';
|
||||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||||
import { BackgroundRenderLayer } from './BackgroundRenderLayer';
|
import { TextRenderLayer } from './TextRenderLayer';
|
||||||
import { ForegroundRenderLayer } from './ForegroundRenderLayer';
|
|
||||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||||
import { ColorManager } from './ColorManager';
|
import { ColorManager } from './ColorManager';
|
||||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||||
import { IRenderLayer, IColorSet, IRenderer } from './Interfaces';
|
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
|
||||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
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 */
|
/** A queue of the rows to be refreshed */
|
||||||
private _refreshRowsQueue: {start: number, end: number}[] = [];
|
private _refreshRowsQueue: {start: number, end: number}[] = [];
|
||||||
private _refreshAnimationFrame = null;
|
private _refreshAnimationFrame = null;
|
||||||
@@ -22,17 +22,31 @@ export class Renderer implements IRenderer {
|
|||||||
private _renderLayers: IRenderLayer[];
|
private _renderLayers: IRenderLayer[];
|
||||||
private _devicePixelRatio: number;
|
private _devicePixelRatio: number;
|
||||||
|
|
||||||
private _colorManager: ColorManager;
|
public colorManager: ColorManager;
|
||||||
|
public dimensions: IRenderDimensions;
|
||||||
|
|
||||||
constructor(private _terminal: ITerminal) {
|
constructor(private _terminal: ITerminal, theme: ITheme) {
|
||||||
this._colorManager = new ColorManager();
|
super();
|
||||||
|
this.colorManager = new ColorManager();
|
||||||
|
if (theme) {
|
||||||
|
this.colorManager.setTheme(theme);
|
||||||
|
}
|
||||||
this._renderLayers = [
|
this._renderLayers = [
|
||||||
new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors),
|
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
|
||||||
new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors),
|
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
|
||||||
new ForegroundRenderLayer(this._terminal.element, 2, this._colorManager.colors),
|
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
|
||||||
new LinkRenderLayer(this._terminal.element, 3, this._colorManager.colors, this._terminal),
|
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
|
||||||
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;
|
this._devicePixelRatio = window.devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,29 +60,68 @@ export class Renderer implements IRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setTheme(theme: ITheme): IColorSet {
|
public setTheme(theme: ITheme): IColorSet {
|
||||||
this._colorManager.setTheme(theme);
|
this.colorManager.setTheme(theme);
|
||||||
|
|
||||||
// Clear layers and force a full render
|
// Clear layers and force a full render
|
||||||
this._renderLayers.forEach(l => {
|
this._renderLayers.forEach(l => {
|
||||||
l.onThemeChanged(this._terminal, this._colorManager.colors);
|
l.onThemeChanged(this._terminal, this.colorManager.colors);
|
||||||
l.reset(this._terminal);
|
l.reset(this._terminal);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||||
|
|
||||||
return this._colorManager.colors;
|
return this.colorManager.colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
|
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
|
||||||
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
|
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
|
||||||
return;
|
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 floored as it must be
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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 = 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
|
// 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
|
// Force a refresh
|
||||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||||
|
|
||||||
|
this.emit('resize', {
|
||||||
|
width: this.dimensions.canvasWidth,
|
||||||
|
height: this.dimensions.canvasHeight
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCharSizeChanged(): void {
|
public onCharSizeChanged(): void {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet } from './Interfaces';
|
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||||
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
|
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
|
||||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||||
import { GridCache } from './GridCache';
|
import { GridCache } from './GridCache';
|
||||||
@@ -14,15 +14,15 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
private _state: {start: [number, number], end: [number, number]};
|
private _state: {start: [number, number], end: [number, number]};
|
||||||
|
|
||||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||||
super(container, 'selection', zIndex, colors);
|
super(container, 'selection', zIndex, true, colors);
|
||||||
this._state = {
|
this._state = {
|
||||||
start: null,
|
start: null,
|
||||||
end: null
|
end: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
super.resize(terminal, dim, charSizeChanged);
|
||||||
// Resizing the canvas discards the contents of the canvas so clear state
|
// Resizing the canvas discards the contents of the canvas so clear state
|
||||||
this._state = {
|
this._state = {
|
||||||
start: null,
|
start: null,
|
||||||
@@ -68,7 +68,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
// Draw first row
|
// Draw first row
|
||||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||||
this._ctx.fillStyle = this.colors.selection;
|
this._ctx.fillStyle = this._colors.selection;
|
||||||
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
||||||
|
|
||||||
// Draw middle rows
|
// Draw middle rows
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet } from './Interfaces';
|
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||||
import { IBuffer, ICharMeasure, ITerminal } 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 { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
||||||
import { FLAGS } from './Types';
|
import { FLAGS } from './Types';
|
||||||
@@ -16,18 +16,29 @@ import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
|
|||||||
* when the character changes (a regular space ' ' character may not as it's
|
* when the character changes (a regular space ' ' character may not as it's
|
||||||
* drawn state is a cleared cell).
|
* drawn state is a cleared cell).
|
||||||
*/
|
*/
|
||||||
const EMOJI_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
|
const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
|
||||||
|
|
||||||
export class ForegroundRenderLayer extends BaseRenderLayer {
|
export class TextRenderLayer extends BaseRenderLayer {
|
||||||
private _state: GridCache<CharData>;
|
private _state: GridCache<CharData>;
|
||||||
|
private _characterWidth: number;
|
||||||
|
private _characterFont: string;
|
||||||
|
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||||
super(container, 'fg', zIndex, colors);
|
super(container, 'text', zIndex, false, colors);
|
||||||
this._state = new GridCache<CharData>();
|
this._state = new GridCache<CharData>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
|
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||||
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
|
super.resize(terminal, dim, charSizeChanged);
|
||||||
|
|
||||||
|
// Clear the character width cache if the font or width has changed
|
||||||
|
const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||||
|
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
|
||||||
|
this._characterWidth = dim.scaledCharWidth;
|
||||||
|
this._characterFont = terminalFont;
|
||||||
|
this._characterOverlapCache = {};
|
||||||
|
}
|
||||||
// Resizing the canvas discards the contents of the canvas so clear state
|
// Resizing the canvas discards the contents of the canvas so clear state
|
||||||
this._state.clear();
|
this._state.clear();
|
||||||
this._state.resize(terminal.cols, terminal.rows);
|
this._state.resize(terminal.cols, terminal.rows);
|
||||||
@@ -63,12 +74,12 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the character is a space and the character to the left is an
|
// If the character is a space and the character to the left is an
|
||||||
// emoji, skip the character and allow the emoji char to take full
|
// overlapping character, skip the character and allow the overlapping
|
||||||
// control over this character's cell.
|
// char to take full control over this character's cell.
|
||||||
if (code === 32 /*' '*/) {
|
if (code === 32 /*' '*/) {
|
||||||
if (x > 0) {
|
if (x > 0) {
|
||||||
const previousChar: CharData = line[x - 1];
|
const previousChar: CharData = line[x - 1];
|
||||||
if (this._isEmoji(previousChar[CHAR_DATA_CHAR_INDEX])) {
|
if (this._isOverlapping(previousChar)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,48 +93,65 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the old character if present
|
// Clear the old character was not a space with the default background
|
||||||
if (state && state[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE);
|
||||||
|
if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) {
|
||||||
this._clearChar(x, y);
|
this._clearChar(x, y);
|
||||||
}
|
}
|
||||||
this._state.cache[x][y] = charData;
|
this._state.cache[x][y] = charData;
|
||||||
|
|
||||||
const flags = attr >> 18;
|
const flags = attr >> 18;
|
||||||
|
let bg = attr & 0x1ff;
|
||||||
|
|
||||||
// Skip rendering if the character is invisible
|
// Skip rendering if the character is invisible
|
||||||
if (!code || code === 32 /*' '*/ || (flags & FLAGS.INVISIBLE)) {
|
const isDefaultBackground = bg >= 256;
|
||||||
|
const isInvisible = flags & FLAGS.INVISIBLE;
|
||||||
|
const isInverted = flags & FLAGS.INVERSE;
|
||||||
|
if (!code || (code === 32 /*' '*/ && isDefaultBackground && !isInverted) || isInvisible) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the character is an emoji and the character to the right is a
|
// If the character is an overlapping char and the character to the right is a
|
||||||
// space, take ownership of the cell to the right.
|
// space, take ownership of the cell to the right.
|
||||||
if (this._isEmoji(char)) {
|
if (width !== 0 && this._isOverlapping(charData)) {
|
||||||
// If the character is an emoji, we want to force a re-render on every
|
// If the character is overlapping, we want to force a re-render on every
|
||||||
// frame. This is specifically to work around the case where two
|
// frame. This is specifically to work around the case where two
|
||||||
// emoji's `a` and `b` are adjacent, the cursor is moved to b and a
|
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
||||||
// space is added. Without this, the first half of `b` would never
|
// space is added. Without this, the first half of `b` would never
|
||||||
// get removed, and `a` would not re-render because it thinks it's
|
// get removed, and `a` would not re-render because it thinks it's
|
||||||
// already in the correct state.
|
// already in the correct state.
|
||||||
this._state.cache[x][y] = EMOJI_OWNED_CHAR_DATA;
|
this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||||
if (x < line.length && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
|
if (x < line.length && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
|
||||||
width = 2;
|
width = 2;
|
||||||
this._clearChar(x + 1, y);
|
this._clearChar(x + 1, y);
|
||||||
// The emoji owned char data will force a clear and render when the
|
// The overlapping char's char data will force a clear and render when the
|
||||||
// emoji is no longer to the left of the character and also when the
|
// overlapping char is no longer to the left of the character and also when
|
||||||
// space changes to another character.
|
// the space changes to another character.
|
||||||
this._state.cache[x + 1][y] = EMOJI_OWNED_CHAR_DATA;
|
this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fg = (attr >> 9) & 0x1ff;
|
let fg = (attr >> 9) & 0x1ff;
|
||||||
|
|
||||||
// If inverse flag is on, the foreground should become the background.
|
// If inverse flag is on, the foreground should become the background.
|
||||||
if (flags & FLAGS.INVERSE) {
|
if (isInverted) {
|
||||||
fg = attr & 0x1ff;
|
const temp = bg;
|
||||||
// TODO: Is this case still needed
|
bg = fg;
|
||||||
|
fg = temp;
|
||||||
if (fg === 256) {
|
if (fg === 256) {
|
||||||
fg = INVERTED_DEFAULT_COLOR;
|
fg = INVERTED_DEFAULT_COLOR;
|
||||||
}
|
}
|
||||||
|
if (bg === 257) {
|
||||||
|
bg = INVERTED_DEFAULT_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
if (bg < 256) {
|
||||||
|
this._ctx.save();
|
||||||
|
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground : this._colors.ansi[bg]);
|
||||||
|
this.fillCells(x, y, width, 1);
|
||||||
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
@@ -137,29 +165,55 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
|||||||
|
|
||||||
if (flags & FLAGS.UNDERLINE) {
|
if (flags & FLAGS.UNDERLINE) {
|
||||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||||
this._ctx.fillStyle = this.colors.background;
|
this._ctx.fillStyle = this._colors.background;
|
||||||
} else if (fg < 256) {
|
} else if (fg < 256) {
|
||||||
// 256 color support
|
// 256 color support
|
||||||
this._ctx.fillStyle = this.colors.ansi[fg];
|
this._ctx.fillStyle = this._colors.ansi[fg];
|
||||||
} else {
|
} else {
|
||||||
this._ctx.fillStyle = this.colors.foreground;
|
this._ctx.fillStyle = this._colors.foreground;
|
||||||
}
|
}
|
||||||
this.fillBottomLineAtCells(x, y);
|
this.fillBottomLineAtCells(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawChar(terminal, char, code, width, x, y, fg, !!(flags & FLAGS.BOLD));
|
this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD));
|
||||||
|
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the character is an emoji.
|
* Whether a character is overlapping to the
|
||||||
* @param char The character to search.
|
* next cell.
|
||||||
*/
|
*/
|
||||||
private _isEmoji(char: string): boolean {
|
private _isOverlapping(charData: CharData): boolean {
|
||||||
return char.search(/([\uD800-\uDBFF][\uDC00-\uDFFF])/g) >= 0;
|
// We assume that any ascii character will not overlap
|
||||||
|
const code = charData[CHAR_DATA_CODE_INDEX];
|
||||||
|
if (code < 256) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver from cache if available
|
||||||
|
const char = charData[CHAR_DATA_CHAR_INDEX];
|
||||||
|
if (this._characterOverlapCache.hasOwnProperty(char)) {
|
||||||
|
return this._characterOverlapCache[char];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the font
|
||||||
|
this._ctx.save();
|
||||||
|
this._ctx.font = this._characterFont;
|
||||||
|
|
||||||
|
// Measure the width of the character, but Math.floor it
|
||||||
|
// because that is what the renderer does when it calculates
|
||||||
|
// the character dimensions we are comparing against
|
||||||
|
const overlaps = Math.floor(this._ctx.measureText(char).width) > this._characterWidth;
|
||||||
|
|
||||||
|
// Restore the original context
|
||||||
|
this._ctx.restore();
|
||||||
|
|
||||||
|
// Cache and return
|
||||||
|
this._characterOverlapCache[char] = overlaps;
|
||||||
|
return overlaps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +60,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._width !== geometry.width || this._height !== geometry.height) {
|
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._height = Math.ceil(geometry.height);
|
||||||
this.emit('charsizechanged');
|
this.emit('charsizechanged');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ describe('getCoords', () => {
|
|||||||
assert.deepEqual(coords, [1, 1]);
|
assert.deepEqual(coords, [1, 1]);
|
||||||
// Event are double the cols/rows
|
// Event are double the cols/rows
|
||||||
coords = getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10);
|
coords = getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10);
|
||||||
assert.deepEqual(coords, [11, 11], 'coordinates should never come back as larger than the terminal');
|
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -53,8 +53,8 @@ export function getCoords(event: {pageX: number, pageY: number}, element: HTMLEl
|
|||||||
coords[1] = Math.ceil(coords[1] / Math.ceil(charMeasure.height * lineHeight));
|
coords[1] = Math.ceil(coords[1] / Math.ceil(charMeasure.height * lineHeight));
|
||||||
|
|
||||||
// Ensure coordinates are within the terminal viewport.
|
// Ensure coordinates are within the terminal viewport.
|
||||||
coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1);
|
coords[0] = Math.min(Math.max(coords[0], 1), colCount);
|
||||||
coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1);
|
coords[1] = Math.min(Math.max(coords[1], 1), rowCount);
|
||||||
|
|
||||||
return coords;
|
return coords;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManag
|
|||||||
import { LineData } from '../Types';
|
import { LineData } from '../Types';
|
||||||
import { Buffer } from '../Buffer';
|
import { Buffer } from '../Buffer';
|
||||||
import * as Browser from './Browser';
|
import * as Browser from './Browser';
|
||||||
import { IColorSet, IRenderer } from '../renderer/Interfaces';
|
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces';
|
||||||
|
|
||||||
export class MockTerminal implements ITerminal {
|
export class MockTerminal implements ITerminal {
|
||||||
|
renderer: IRenderer;
|
||||||
linkifier: ILinkifier;
|
linkifier: ILinkifier;
|
||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
options: ITerminalOptions = {};
|
options: ITerminalOptions = {};
|
||||||
@@ -215,6 +216,17 @@ export class MockBuffer implements IBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MockRenderer implements IRenderer {
|
export class MockRenderer implements IRenderer {
|
||||||
|
colorManager: IColorManager;
|
||||||
|
on(type: string, listener: IListenerType): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
off(type: string, listener: IListenerType): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
emit(type: string, data?: any): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
dimensions: IRenderDimensions;
|
||||||
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
|
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
|
||||||
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
|
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
|
||||||
onCharSizeChanged(): void {}
|
onCharSizeChanged(): void {}
|
||||||
|
|||||||
Vendored
+2
@@ -88,6 +88,8 @@ interface ITheme {
|
|||||||
background?: string,
|
background?: string,
|
||||||
/** The cursor color */
|
/** The cursor color */
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
|
/** The accent color of the cursor (used as the foreground color for a block cursor) */
|
||||||
|
cursorAccent?: string,
|
||||||
/** The selection color (can be transparent) */
|
/** The selection color (can be transparent) */
|
||||||
selection?: string,
|
selection?: string,
|
||||||
/** ANSI black (eg. `\x1b[30m`) */
|
/** ANSI black (eg. `\x1b[30m`) */
|
||||||
|
|||||||
Reference in New Issue
Block a user