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:
+14
-11
@@ -622,8 +622,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
|
||||
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.renderer = new Renderer(this);
|
||||
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
|
||||
|
||||
this.on('cursormove', () => this.renderer.onCursorMove());
|
||||
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
|
||||
this.on('blur', () => this.renderer.onBlur());
|
||||
@@ -652,15 +655,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// Measure the character size
|
||||
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
|
||||
this.refresh(0, this.rows - 1);
|
||||
|
||||
@@ -1078,7 +1072,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
if (!willBufferBeTrimmed) {
|
||||
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 {
|
||||
// scrollTop is non-zero which means no line will be going to the
|
||||
|
||||
@@ -102,10 +102,14 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
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) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = null;
|
||||
if (this._tooltipTimeout) {
|
||||
clearTimeout(this._tooltipTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
// Exit if there is not zone
|
||||
@@ -119,14 +123,12 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
zone.hoverCallback(e);
|
||||
}
|
||||
|
||||
// Restart the timeout
|
||||
if (this._tooltipTimeout) {
|
||||
clearTimeout(this._tooltipTimeout);
|
||||
}
|
||||
// Restart the tooltip timeout
|
||||
this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
|
||||
}
|
||||
|
||||
private _onTooltip(e: MouseEvent): void {
|
||||
this._tooltipTimeout = null;
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone && zone.tooltipCallback) {
|
||||
zone.tooltipCallback(e);
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet, IRenderDimensions } 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, 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,10 @@ export const INVERTED_DEFAULT_COLOR = -1;
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx: CanvasRenderingContext2D;
|
||||
private scaledCharWidth: number;
|
||||
private scaledCharHeight: number;
|
||||
private scaledLineHeight: number;
|
||||
private scaledLineDrawY: number;
|
||||
private _scaledCharWidth: number;
|
||||
private _scaledCharHeight: number;
|
||||
private _scaledLineHeight: number;
|
||||
private _scaledLineDrawY: number;
|
||||
|
||||
private _charAtlas: HTMLCanvasElement | ImageBitmap;
|
||||
|
||||
@@ -25,13 +25,18 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
protected colors: IColorSet
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.id = `xterm-${id}-layer`;
|
||||
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);
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!_alpha) {
|
||||
this.clearAll();
|
||||
}
|
||||
container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
@@ -53,7 +58,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void {
|
||||
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) {
|
||||
this._charAtlas = result;
|
||||
} else {
|
||||
@@ -62,17 +67,22 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
|
||||
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._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`;
|
||||
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this.clearAll();
|
||||
}
|
||||
|
||||
if (charSizeChanged) {
|
||||
this._refreshCharAtlas(terminal, this.colors);
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +95,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param x The column of the cell.
|
||||
*/
|
||||
private _getCellLeft(x: number): number {
|
||||
return Math.round(x * this.scaledCharWidth);
|
||||
return Math.round(x * this._scaledCharWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,9 +109,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const cellLeft = this._getCellLeft(x);
|
||||
this._ctx.fillRect(
|
||||
cellLeft,
|
||||
y * this.scaledLineHeight,
|
||||
y * this._scaledLineHeight,
|
||||
this._getCellLeft(x + width) - cellLeft,
|
||||
height * this.scaledLineHeight);
|
||||
height * this._scaledLineHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +124,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const cellLeft = this._getCellLeft(x);
|
||||
this._ctx.fillRect(
|
||||
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 */,
|
||||
this._getCellLeft(x + width) - cellLeft,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
@@ -128,9 +138,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected fillLeftLineAtCell(x: number, y: number): void {
|
||||
this._ctx.fillRect(
|
||||
this._getCellLeft(x),
|
||||
y * this.scaledLineHeight,
|
||||
y * this._scaledLineHeight,
|
||||
window.devicePixelRatio,
|
||||
this.scaledLineHeight);
|
||||
this._scaledLineHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,16 +154,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.strokeRect(
|
||||
cellLeft + window.devicePixelRatio / 2,
|
||||
y * this.scaledLineHeight + (window.devicePixelRatio / 2),
|
||||
y * this._scaledLineHeight + (window.devicePixelRatio / 2),
|
||||
this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio,
|
||||
(height * this.scaledLineHeight) - window.devicePixelRatio);
|
||||
(height * this._scaledLineHeight) - window.devicePixelRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire canvas.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,11 +180,20 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected clearCells(x: number, y: number, width: number, height: number): void {
|
||||
const cellLeft = this._getCellLeft(x);
|
||||
this._ctx.clearRect(
|
||||
cellLeft,
|
||||
y * this.scaledLineHeight,
|
||||
this._getCellLeft(x + width) - cellLeft,
|
||||
height * this.scaledLineHeight);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,9 +215,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
// can bleed into other cells. This code will clip the following fillText,
|
||||
// ensuring that its contents don't go beyond the cell bounds.
|
||||
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.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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,9 +230,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @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.
|
||||
*/
|
||||
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
|
||||
if (width === 2) {
|
||||
this.clearCells(x + 1, y, 1, 1);
|
||||
@@ -226,15 +252,16 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const isAscii = code < 256;
|
||||
const isBasicColor = (colorIndex > 1 && fg < 16);
|
||||
const isDefaultColor = fg >= 256;
|
||||
if (isAscii && (isBasicColor || isDefaultColor)) {
|
||||
const isDefaultBackground = bg >= 256;
|
||||
if (isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
|
||||
// ImageBitmap's draw about twice as fast as from a canvas
|
||||
const charAtlasCellWidth = this.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
||||
const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
||||
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
||||
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
||||
this._ctx.drawImage(this._charAtlas,
|
||||
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight,
|
||||
x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, this.scaledCharWidth, this.scaledCharHeight);
|
||||
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this._scaledCharWidth, this._scaledCharHeight,
|
||||
x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight);
|
||||
} 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._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
@@ -252,18 +279,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param x The column 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.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
if (bold) {
|
||||
this._ctx.font = `bold ${this._ctx.font}`;
|
||||
}
|
||||
this._ctx.textBaseline = 'top';
|
||||
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this.colors.background;
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
} else if (fg < 256) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this.colors.ansi[fg];
|
||||
this._ctx.fillStyle = this._colors.ansi[fg];
|
||||
} 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
|
||||
@@ -271,11 +301,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
// can bleed into other cells. This code will clip the following fillText,
|
||||
// ensuring that its contents don't go beyond the cell bounds.
|
||||
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();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
|
||||
}
|
||||
|
||||
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,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
@@ -75,7 +75,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
|
||||
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
|
||||
const clonedColors = {
|
||||
foreground: colors.foreground,
|
||||
background: null,
|
||||
background: colors.background,
|
||||
cursor: null,
|
||||
cursorAccent: null,
|
||||
selection: null,
|
||||
@@ -100,7 +100,8 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
a.fontSize === b.fontSize &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
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;
|
||||
@@ -121,16 +122,19 @@ class CharAtlasGenerator {
|
||||
|
||||
constructor(private _document: Document) {
|
||||
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);
|
||||
}
|
||||
|
||||
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 cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
||||
this._canvas.width = 255 * cellWidth;
|
||||
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.fillStyle = foreground;
|
||||
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IColorSet, IColorManager } from './Interfaces';
|
||||
import { ITheme } from '../Interfaces';
|
||||
|
||||
const DEFAULT_FOREGROUND = '#ffffff';
|
||||
@@ -65,7 +65,7 @@ function toPaddedHex(c: number): string {
|
||||
/**
|
||||
* Manages the source of truth for a terminal's colors.
|
||||
*/
|
||||
export class ColorManager {
|
||||
export class ColorManager implements IColorManager {
|
||||
public colors: IColorSet;
|
||||
|
||||
constructor() {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _isFocused: boolean;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'cursor', zIndex, colors);
|
||||
super(container, 'cursor', zIndex, true, colors);
|
||||
this._state = {
|
||||
x: null,
|
||||
y: null,
|
||||
@@ -135,7 +135,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (!terminal.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this.colors.cursor;
|
||||
this._ctx.fillStyle = this._colors.cursor;
|
||||
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
||||
this._ctx.restore();
|
||||
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 {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this.colors.cursor;
|
||||
this._ctx.fillStyle = this._colors.cursor;
|
||||
this.fillLeftLineAtCell(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
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._ctx.fillStyle = this.colors.cursorAccent;
|
||||
this._ctx.fillStyle = this._colors.cursorAccent;
|
||||
this.fillCharTrueColor(terminal, charData, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this.colors.cursor;
|
||||
this._ctx.fillStyle = this._colors.cursor;
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
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._ctx.restore();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interface
|
||||
|
||||
export interface IRenderer extends IEventEmitter {
|
||||
dimensions: IRenderDimensions;
|
||||
colorManager: IColorManager;
|
||||
|
||||
setTheme(theme: ITheme): IColorSet;
|
||||
onWindowResize(devicePixelRatio: number): void;
|
||||
@@ -69,6 +70,9 @@ export interface IRenderLayer {
|
||||
reset(terminal: ITerminal): void;
|
||||
}
|
||||
|
||||
export interface IColorManager {
|
||||
colors: IColorSet;
|
||||
}
|
||||
|
||||
export interface IColorSet {
|
||||
foreground: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: LinkHoverEvent = null;
|
||||
|
||||
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.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
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._state = e;
|
||||
}
|
||||
|
||||
+20
-21
@@ -5,8 +5,7 @@
|
||||
|
||||
import { ITerminal, ITheme } from '../Interfaces';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { BackgroundRenderLayer } from './BackgroundRenderLayer';
|
||||
import { ForegroundRenderLayer } from './ForegroundRenderLayer';
|
||||
import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { ColorManager } from './ColorManager';
|
||||
@@ -23,18 +22,20 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
private _colorManager: ColorManager;
|
||||
public colorManager: ColorManager;
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
constructor(private _terminal: ITerminal, theme: ITheme) {
|
||||
super();
|
||||
this._colorManager = new ColorManager();
|
||||
this.colorManager = new ColorManager();
|
||||
if (theme) {
|
||||
this.colorManager.setTheme(theme);
|
||||
}
|
||||
this._renderLayers = [
|
||||
new BackgroundRenderLayer(this._terminal.element, 0, 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, 3, this._colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.element, 4, this._colorManager.colors)
|
||||
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
|
||||
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: null,
|
||||
@@ -59,17 +60,17 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
}
|
||||
|
||||
public setTheme(theme: ITheme): IColorSet {
|
||||
this._colorManager.setTheme(theme);
|
||||
this.colorManager.setTheme(theme);
|
||||
|
||||
// Clear layers and force a full render
|
||||
this._renderLayers.forEach(l => {
|
||||
l.onThemeChanged(this._terminal, this._colorManager.colors);
|
||||
l.onThemeChanged(this._terminal, this.colorManager.colors);
|
||||
l.reset(this._terminal);
|
||||
});
|
||||
|
||||
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 {
|
||||
@@ -77,13 +78,11 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 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
|
||||
@@ -103,7 +102,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
// 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);
|
||||
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
|
||||
|
||||
@@ -14,7 +14,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
private _state: {start: [number, number], end: [number, number]};
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'selection', zIndex, colors);
|
||||
super(container, 'selection', zIndex, true, colors);
|
||||
this._state = {
|
||||
start: null,
|
||||
end: null
|
||||
@@ -68,7 +68,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
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);
|
||||
|
||||
// Draw middle rows
|
||||
|
||||
@@ -18,11 +18,11 @@ import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
|
||||
*/
|
||||
const EMOJI_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
|
||||
|
||||
export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _state: GridCache<CharData>;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'fg', zIndex, colors);
|
||||
super(container, 'text', zIndex, false, colors);
|
||||
this._state = new GridCache<CharData>();
|
||||
}
|
||||
|
||||
@@ -82,16 +82,19 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clear the old character if present
|
||||
if (state && state[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
// Clear the old character was not a space with the default background
|
||||
if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256)) {
|
||||
this._clearChar(x, y);
|
||||
}
|
||||
this._state.cache[x][y] = charData;
|
||||
|
||||
const flags = attr >> 18;
|
||||
let bg = attr & 0x1ff;
|
||||
|
||||
// Skip rendering if the character is invisible
|
||||
if (!code || code === 32 /*' '*/ || (flags & FLAGS.INVISIBLE)) {
|
||||
const isDefaultBackground = bg >= 256;
|
||||
const isInvisible = flags & FLAGS.INVISIBLE;
|
||||
if (!code || (code === 32 /*' '*/ && isDefaultBackground) || isInvisible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -119,11 +122,23 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// If inverse flag is on, the foreground should become the background.
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
fg = attr & 0x1ff;
|
||||
// TODO: Is this case still needed
|
||||
const temp = bg;
|
||||
bg = fg;
|
||||
fg = bg;
|
||||
if (fg === 256) {
|
||||
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();
|
||||
@@ -137,17 +152,17 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
|
||||
if (flags & FLAGS.UNDERLINE) {
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this.colors.background;
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
} else if (fg < 256) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this.colors.ansi[fg];
|
||||
this._ctx.fillStyle = this._colors.ansi[fg];
|
||||
} else {
|
||||
this._ctx.fillStyle = this.colors.foreground;
|
||||
this._ctx.fillStyle = this._colors.foreground;
|
||||
}
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user