mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Support wide character drawing and cursors
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { IRenderLayer, IColorSet } from './Interfaces';
|
||||
import { ITerminal, ITerminalOptions } from '../Interfaces';
|
||||
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
|
||||
import { CharData } from '../Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
|
||||
export const INVERTED_DEFAULT_COLOR = -1;
|
||||
|
||||
@@ -76,14 +78,14 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this.scaledCharHeight);
|
||||
}
|
||||
|
||||
protected drawSquareAtCell(x: number, y: number, color: string): void {
|
||||
protected drawRectAtCell(x: number, y: number, width: number, height: number, color: string): void {
|
||||
this._ctx.strokeStyle = color;
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.strokeRect(
|
||||
x * this.scaledCharWidth + window.devicePixelRatio / 2,
|
||||
y * this.scaledCharHeight + (window.devicePixelRatio / 2),
|
||||
this.scaledCharWidth - window.devicePixelRatio,
|
||||
this.scaledCharHeight - window.devicePixelRatio);
|
||||
(width * this.scaledCharWidth) - window.devicePixelRatio,
|
||||
(height * this.scaledCharHeight) - window.devicePixelRatio);
|
||||
}
|
||||
|
||||
protected clearAll(): void {
|
||||
@@ -94,16 +96,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight);
|
||||
}
|
||||
|
||||
protected drawCharTrueColor(terminal: ITerminal, char: string, code: number, x: number, y: number, color: string): void {
|
||||
protected drawCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number, color: string): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
this._ctx.textBaseline = 'top';
|
||||
this._ctx.fillStyle = color;
|
||||
this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledCharHeight);
|
||||
|
||||
// Since uncached characters are not coming off the char atlas with source
|
||||
// coordinates, it means that text drawn to the canvas (particularly '_')
|
||||
// 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.scaledCharHeight, 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.restore();
|
||||
}
|
||||
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, x: number, y: number, fg: number, underline: boolean = false): void {
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, underline: boolean = false): void {
|
||||
let colorIndex = 0;
|
||||
if (fg < 256) {
|
||||
colorIndex = fg + 1;
|
||||
@@ -116,13 +127,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight,
|
||||
x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight);
|
||||
} else {
|
||||
this._drawUncachedChar(terminal, char, fg, x, y);
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y);
|
||||
}
|
||||
// This draws the atlas (for debugging purposes)
|
||||
// this._ctx.drawImage(this._charAtlas, 0, 0);
|
||||
}
|
||||
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, fg: number, x: number, y: number): void {
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
this._ctx.textBaseline = 'top';
|
||||
@@ -141,7 +152,7 @@ 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.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight);
|
||||
this._ctx.rect(x * this.scaledCharWidth, y * this.scaledCharHeight, width * this.scaledCharWidth, this.scaledCharHeight);
|
||||
this._ctx.clip();
|
||||
|
||||
// Draw the character
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
import { IColorSet } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
|
||||
import { 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 { FLAGS } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { CharData } from '../Types';
|
||||
import { COLOR_CODES } from './ColorManager';
|
||||
|
||||
interface CursorState {
|
||||
x: number;
|
||||
y: number;
|
||||
isFocused: boolean;
|
||||
style: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time between cursor blinks.
|
||||
*/
|
||||
const BLINK_INTERVAL = 600;
|
||||
|
||||
export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _state: [number, number, boolean, string];
|
||||
private _state: CursorState;
|
||||
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
|
||||
private _cursorBlinkStateManager: CursorBlinkStateManager;
|
||||
private _isFocused: boolean;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'cursor', zIndex, colors);
|
||||
this._state = null;
|
||||
this._state = {
|
||||
x: null,
|
||||
y: null,
|
||||
isFocused: null,
|
||||
style: null,
|
||||
width: null,
|
||||
};
|
||||
this._cursorRenderers = {
|
||||
'bar': this._renderBarCursor.bind(this),
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
@@ -42,14 +56,15 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.pause();
|
||||
}
|
||||
terminal.emit('cursormove');
|
||||
terminal.refresh(terminal.buffer.y, terminal.buffer.y);
|
||||
}
|
||||
|
||||
public onFocus(terminal: ITerminal): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.resume();
|
||||
this._cursorBlinkStateManager.resume(terminal);
|
||||
} else {
|
||||
terminal.refresh(terminal.buffer.y, terminal.buffer.y);
|
||||
}
|
||||
terminal.emit('cursormove');
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: ITerminal): void {
|
||||
@@ -107,7 +122,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE];
|
||||
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
||||
this._ctx.restore();
|
||||
this._state = [terminal.buffer.x, viewportRelativeCursorY, false, terminal.options.cursorStyle];
|
||||
this._state.x = terminal.buffer.x;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = terminal.options.cursorStyle;
|
||||
this._state.width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -119,10 +138,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
if (this._state) {
|
||||
// The cursor is already in the correct spot, don't redraw
|
||||
if (this._state[0] === terminal.buffer.x &&
|
||||
this._state[1] === viewportRelativeCursorY &&
|
||||
this._state[2] === terminal.isFocused &&
|
||||
this._state[3] === terminal.options.cursorStyle) {
|
||||
if (this._state.x === terminal.buffer.x &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === terminal.isFocused &&
|
||||
this._state.style === terminal.options.cursorStyle &&
|
||||
this._state.width === charData[CHAR_DATA_WIDTH_INDEX]) {
|
||||
return;
|
||||
}
|
||||
this._clearCursor();
|
||||
@@ -132,13 +152,24 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE];
|
||||
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
||||
this._ctx.restore();
|
||||
this._state = [terminal.buffer.x, viewportRelativeCursorY, true, terminal.options.cursorStyle];
|
||||
|
||||
this._state.x = terminal.buffer.x;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = terminal.options.cursorStyle;
|
||||
this._state.width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
}
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
this.clearCells(this._state[0], this._state[1], 1, 1);
|
||||
this._state = null;
|
||||
this.clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
this._state = {
|
||||
x: null,
|
||||
y: null,
|
||||
isFocused: null,
|
||||
style: null,
|
||||
width: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,9 +183,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this.colors.cursor;
|
||||
this.fillCells(x, y, 1, 1);
|
||||
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
||||
this._ctx.restore();
|
||||
this.drawCharTrueColor(terminal, charData[CHAR_DATA_CHAR_INDEX], <number>charData[CHAR_DATA_CODE_INDEX], x, y, this.colors.background);
|
||||
this.drawCharTrueColor(terminal, charData, x, y, this.colors.background);
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
@@ -166,7 +197,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this.drawSquareAtCell(x, y, this.colors.cursor);
|
||||
this.drawRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1, this.colors.cursor);
|
||||
this._ctx.restore();
|
||||
}
|
||||
}
|
||||
@@ -295,8 +326,9 @@ class CursorBlinkStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
public resume(terminal: ITerminal): void {
|
||||
this._animationTimeRestarted = null;
|
||||
this._restartInterval();
|
||||
this.restartBlinkAnimation(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
|
||||
this.drawBottomLineAtCell(x, y);
|
||||
}
|
||||
|
||||
this.drawChar(terminal, char, code, x, y, fg);
|
||||
const width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
this.drawChar(terminal, char, code, width, x, y, fg);
|
||||
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user