mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { ICharacterJoinerService, IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
|
||||
@@ -25,8 +25,9 @@ export class WebglAddon implements ITerminalAddon {
|
||||
}
|
||||
this._terminal = terminal;
|
||||
const renderService: IRenderService = (<any>terminal)._core._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = (<any>terminal)._core._characterJoinerService;
|
||||
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer);
|
||||
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer);
|
||||
this._renderer.onContextLoss(() => this._onContextLoss.fire());
|
||||
renderService.setRenderer(this._renderer);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IEvent } from 'xterm';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
@@ -20,6 +20,9 @@ import { ITerminal, IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
@@ -48,6 +51,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
@@ -288,16 +292,41 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
private _updateModel(start: number, end: number): void {
|
||||
const terminal = this._core;
|
||||
let cell: ICellData = this._workCell;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row)!;
|
||||
this._model.lineLengths[y] = 0;
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
line.loadCell(x, this._workCell);
|
||||
line.loadCell(x, cell);
|
||||
|
||||
const chars = this._workCell.getChars();
|
||||
let code = this._workCell.getCode();
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift()!;
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
cell = new JoinedCellData(
|
||||
cell,
|
||||
line!.translateToString(true, range[0], range[1]),
|
||||
range[1] - range[0]
|
||||
);
|
||||
|
||||
// Skip over the cells occupied by this range in the loop
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
const chars = cell.getChars();
|
||||
let code = cell.getCode();
|
||||
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
@@ -306,8 +335,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code &&
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) {
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -318,10 +347,24 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Cache the results in the model
|
||||
this._model.cells[i] = code;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars);
|
||||
this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.fg, chars);
|
||||
|
||||
if (isJoined) {
|
||||
// Restore work cell
|
||||
cell = this._workCell;
|
||||
|
||||
// Null out non-first cells
|
||||
for (x++; x < lastCharX; x++) {
|
||||
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
@@ -438,3 +481,49 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Share impl with core
|
||||
export class JoinedCellData extends AttributeData implements ICellData {
|
||||
private _width: number;
|
||||
// .content carries no meaning for joined CellData, simply nullify it
|
||||
// thus we have to overload all other .content accessors
|
||||
public content: number = 0;
|
||||
public fg: number;
|
||||
public bg: number;
|
||||
public combinedData: string = '';
|
||||
|
||||
constructor(firstCell: ICellData, chars: string, width: number) {
|
||||
super();
|
||||
this.fg = firstCell.fg;
|
||||
this.bg = firstCell.bg;
|
||||
this.combinedData = chars;
|
||||
this._width = width;
|
||||
}
|
||||
|
||||
public isCombined(): number {
|
||||
// always mark joined cell data as combined
|
||||
return Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
public getWidth(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public getChars(): string {
|
||||
return this.combinedData;
|
||||
}
|
||||
|
||||
public getCode(): number {
|
||||
// code always gets the highest possible fake codepoint (read as -1)
|
||||
// this is needed as code is used by caches as identifier
|
||||
return 0x1FFFFF;
|
||||
}
|
||||
|
||||
public setFromCharData(value: CharData): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
public getAsCharData(): CharData {
|
||||
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
|
||||
|
||||
this._tmpCanvas = document.createElement('canvas');
|
||||
this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.width = this._config.scaledCharWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
|
||||
}
|
||||
@@ -317,6 +317,13 @@ export class WebglCharAtlas implements IDisposable {
|
||||
|
||||
this.hasCanvasChanged = true;
|
||||
|
||||
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
|
||||
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
|
||||
// giant ligatures (eg. =====>) don't impact overall performance.
|
||||
const allowedWidth = this._config.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.width < allowedWidth) {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
this._tmpCtx.save();
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
@@ -405,7 +412,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, isPowerlineGlyph);
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Check if there is enough room in the current row and go to next if needed
|
||||
@@ -438,14 +445,14 @@ export class WebglCharAtlas implements IDisposable {
|
||||
* @param imageData The image data to read.
|
||||
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, restrictedGlyph: boolean): IRasterizedGlyph {
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.scaledCharWidth : this._tmpCanvas.width;
|
||||
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
|
||||
let found = false;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.top = y;
|
||||
found = true;
|
||||
@@ -460,7 +467,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
found = false;
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.left = x;
|
||||
found = true;
|
||||
@@ -475,7 +482,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
found = false;
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.right = x;
|
||||
found = true;
|
||||
@@ -490,7 +497,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
found = false;
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * width * 4 + x * 4 + 3;
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.bottom = y;
|
||||
found = true;
|
||||
|
||||
@@ -99,7 +99,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
|
||||
cell = new JoinedCellData(
|
||||
this._workCell,
|
||||
line!.translateToString(true, range[0], range[1]),
|
||||
|
||||
@@ -42,7 +42,6 @@ export class DomRendererRowFactory {
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
console.log('joinedRanges', joinedRanges.map(e => e[0] + '->' + e[1]).join(','));
|
||||
// Find the line length first, this prevents the need to output a bunch of
|
||||
// empty cells at the end. This cannot easily be integrated into the main
|
||||
// loop below because of the colCount feature (which can be removed after we
|
||||
@@ -79,7 +78,6 @@ export class DomRendererRowFactory {
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
|
||||
cell = new JoinedCellData(
|
||||
this._workCell,
|
||||
lineData.translateToString(true, range[0], range[1]),
|
||||
|
||||
Reference in New Issue
Block a user