632 lines
25 KiB
TypeScript
632 lines
25 KiB
TypeScript
/**
|
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import { GlyphRenderer } from './GlyphRenderer';
|
|
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
|
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
|
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
|
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
|
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 { Attributes, Content, FgFlags, 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';
|
|
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';
|
|
import { IDecorationService } from 'common/services/Services';
|
|
|
|
export class WebglRenderer extends Disposable implements IRenderer {
|
|
private _renderLayers: IRenderLayer[];
|
|
private _charAtlas: WebglCharAtlas | undefined;
|
|
private _devicePixelRatio: number;
|
|
|
|
private _model: RenderModel = new RenderModel();
|
|
private _workCell: CellData = new CellData();
|
|
private _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 };
|
|
|
|
private _canvas: HTMLCanvasElement;
|
|
private _gl: IWebGL2RenderingContext;
|
|
private _rectangleRenderer: RectangleRenderer;
|
|
private _glyphRenderer: GlyphRenderer;
|
|
|
|
public dimensions: IRenderDimensions;
|
|
|
|
private _core: ITerminal;
|
|
private _isAttached: boolean;
|
|
|
|
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
|
|
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
|
|
|
|
private _onContextLoss = new EventEmitter<void>();
|
|
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
|
|
|
constructor(
|
|
private _terminal: Terminal,
|
|
private _colors: IColorSet,
|
|
private readonly _characterJoinerService: ICharacterJoinerService,
|
|
private readonly _decorationService: IDecorationService,
|
|
preserveDrawingBuffer?: boolean
|
|
) {
|
|
super();
|
|
|
|
this._core = (this._terminal as any)._core;
|
|
|
|
this._renderLayers = [
|
|
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core),
|
|
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw)
|
|
];
|
|
this.dimensions = {
|
|
scaledCharWidth: 0,
|
|
scaledCharHeight: 0,
|
|
scaledCellWidth: 0,
|
|
scaledCellHeight: 0,
|
|
scaledCharLeft: 0,
|
|
scaledCharTop: 0,
|
|
scaledCanvasWidth: 0,
|
|
scaledCanvasHeight: 0,
|
|
canvasWidth: 0,
|
|
canvasHeight: 0,
|
|
actualCellWidth: 0,
|
|
actualCellHeight: 0
|
|
};
|
|
this._devicePixelRatio = window.devicePixelRatio;
|
|
this._updateDimensions();
|
|
|
|
this._canvas = document.createElement('canvas');
|
|
|
|
const contextAttributes = {
|
|
antialias: false,
|
|
depth: false,
|
|
preserveDrawingBuffer
|
|
};
|
|
this._gl = this._canvas.getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
|
if (!this._gl) {
|
|
throw new Error('WebGL2 not supported ' + this._gl);
|
|
}
|
|
|
|
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
|
|
|
|
this._core.screenElement!.appendChild(this._canvas);
|
|
|
|
this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions));
|
|
this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions));
|
|
|
|
// Update dimensions and acquire char atlas
|
|
this.onCharSizeChanged();
|
|
|
|
this._isAttached = document.body.contains(this._core.screenElement!);
|
|
}
|
|
|
|
public dispose(): void {
|
|
for (const l of this._renderLayers) {
|
|
l.dispose();
|
|
}
|
|
this._canvas.parentElement?.removeChild(this._canvas);
|
|
super.dispose();
|
|
}
|
|
|
|
public get textureAtlas(): HTMLCanvasElement | undefined {
|
|
return this._charAtlas?.cacheCanvas;
|
|
}
|
|
|
|
public setColors(colors: IColorSet): void {
|
|
this._colors = colors;
|
|
// Clear layers and force a full render
|
|
for (const l of this._renderLayers) {
|
|
l.setColors(this._terminal, this._colors);
|
|
l.reset(this._terminal);
|
|
}
|
|
|
|
this._rectangleRenderer.setColors();
|
|
this._glyphRenderer.setColors();
|
|
|
|
this._refreshCharAtlas();
|
|
|
|
// Force a full refresh
|
|
this._model.clear();
|
|
}
|
|
|
|
public onDevicePixelRatioChange(): void {
|
|
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
|
// and the terminal needs to refreshed
|
|
if (this._devicePixelRatio !== window.devicePixelRatio) {
|
|
this._devicePixelRatio = window.devicePixelRatio;
|
|
this.onResize(this._terminal.cols, this._terminal.rows);
|
|
}
|
|
}
|
|
|
|
public onResize(cols: number, rows: number): void {
|
|
// Update character and canvas dimensions
|
|
this._updateDimensions();
|
|
|
|
this._model.resize(this._terminal.cols, this._terminal.rows);
|
|
|
|
// Resize all render layers
|
|
for (const l of this._renderLayers) {
|
|
l.resize(this._terminal, this.dimensions);
|
|
}
|
|
|
|
// Resize the canvas
|
|
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
|
this._canvas.height = this.dimensions.scaledCanvasHeight;
|
|
this._canvas.style.width = `${this.dimensions.canvasWidth}px`;
|
|
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
|
|
|
|
// Resize the screen
|
|
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
|
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
|
|
|
this._rectangleRenderer.onResize();
|
|
this._glyphRenderer.setDimensions(this.dimensions);
|
|
this._glyphRenderer.onResize();
|
|
|
|
this._refreshCharAtlas();
|
|
|
|
// Force a full refresh
|
|
this._model.clear();
|
|
}
|
|
|
|
public onCharSizeChanged(): void {
|
|
this.onResize(this._terminal.cols, this._terminal.rows);
|
|
}
|
|
|
|
public onBlur(): void {
|
|
for (const l of this._renderLayers) {
|
|
l.onBlur(this._terminal);
|
|
}
|
|
}
|
|
|
|
public onFocus(): void {
|
|
for (const l of this._renderLayers) {
|
|
l.onFocus(this._terminal);
|
|
}
|
|
}
|
|
|
|
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
|
for (const l of this._renderLayers) {
|
|
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
|
|
}
|
|
this._updateSelectionModel(start, end, columnSelectMode);
|
|
this._requestRedrawViewport();
|
|
}
|
|
|
|
public onCursorMove(): void {
|
|
for (const l of this._renderLayers) {
|
|
l.onCursorMove(this._terminal);
|
|
}
|
|
}
|
|
|
|
public onOptionsChanged(): void {
|
|
for (const l of this._renderLayers) {
|
|
l.onOptionsChanged(this._terminal);
|
|
}
|
|
this._updateDimensions();
|
|
this._refreshCharAtlas();
|
|
}
|
|
|
|
/**
|
|
* Refreshes the char atlas, aquiring a new one if necessary.
|
|
* @param terminal The terminal.
|
|
* @param colorSet The color set to use for the char atlas.
|
|
*/
|
|
private _refreshCharAtlas(): void {
|
|
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
|
|
// Mark as not attached so char atlas gets refreshed on next render
|
|
this._isAttached = false;
|
|
return;
|
|
}
|
|
|
|
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
|
|
if (!('getRasterizedGlyph' in atlas)) {
|
|
throw new Error('The webgl renderer only works with the webgl char atlas');
|
|
}
|
|
this._charAtlas = atlas as WebglCharAtlas;
|
|
this._charAtlas.warmUp();
|
|
this._glyphRenderer.setAtlas(this._charAtlas);
|
|
}
|
|
|
|
public clearCharAtlas(): void {
|
|
this._charAtlas?.clearTexture();
|
|
this._model.clear();
|
|
this._updateModel(0, this._terminal.rows - 1);
|
|
this._requestRedrawViewport();
|
|
}
|
|
|
|
public clear(): void {
|
|
this._model.clear();
|
|
this._glyphRenderer.clear(true);
|
|
for (const l of this._renderLayers) {
|
|
l.reset(this._terminal);
|
|
}
|
|
}
|
|
|
|
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
|
return -1;
|
|
}
|
|
|
|
public deregisterCharacterJoiner(joinerId: number): boolean {
|
|
return false;
|
|
}
|
|
|
|
public renderRows(start: number, end: number): void {
|
|
if (!this._isAttached) {
|
|
if (document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) {
|
|
this._updateDimensions();
|
|
this._refreshCharAtlas();
|
|
this._isAttached = true;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Update render layers
|
|
for (const l of this._renderLayers) {
|
|
l.onGridChanged(this._terminal, start, end);
|
|
}
|
|
|
|
// Tell renderer the frame is beginning
|
|
if (this._glyphRenderer.beginFrame()) {
|
|
this._model.clear();
|
|
this._updateSelectionModel(undefined, undefined);
|
|
}
|
|
|
|
// Update model to reflect what's drawn
|
|
this._updateModel(start, end);
|
|
|
|
// Render
|
|
this._rectangleRenderer.render();
|
|
this._glyphRenderer.render(this._model);
|
|
}
|
|
|
|
private _updateModel(start: number, end: number): void {
|
|
const terminal = this._core;
|
|
let cell: ICellData = this._workCell;
|
|
let lastBg: number = 0;
|
|
|
|
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++) {
|
|
lastBg = this._workColors.bg;
|
|
line.loadCell(x, cell);
|
|
|
|
if (x === 0) {
|
|
lastBg = this._workColors.bg;
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Load colors/resolve overrides into work colors
|
|
this._loadColorsForCell(x, row);
|
|
|
|
if (code !== NULL_CELL_CODE) {
|
|
this._model.lineLengths[y] = x + 1;
|
|
}
|
|
|
|
// Nothing has changed, no updates needed
|
|
if (this._model.cells[i] === code &&
|
|
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
|
|
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) {
|
|
continue;
|
|
}
|
|
|
|
// Flag combined chars with a bit mask so they're easily identifiable
|
|
if (chars.length > 1) {
|
|
code |= COMBINED_CHAR_BIT_MASK;
|
|
}
|
|
|
|
// Cache the results in the model
|
|
this._model.cells[i] = code;
|
|
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
|
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
|
|
|
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg);
|
|
|
|
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, 0, NULL_CELL_CHAR, 0);
|
|
this._model.cells[j] = NULL_CELL_CODE;
|
|
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
|
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this._rectangleRenderer.updateBackgrounds(this._model);
|
|
}
|
|
|
|
/**
|
|
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
|
|
* necessary which is why the work cell object is not used.
|
|
*/
|
|
private _loadColorsForCell(x: number, y: number): void {
|
|
this._workColors.bg = this._workCell.bg;
|
|
this._workColors.fg = this._workCell.fg;
|
|
// TODO: Use extended packed format as key
|
|
this._workColors.ext = this._workCell.extended.underlineStyle;
|
|
|
|
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
|
// override logic throughout the different sub-renderers
|
|
let bgOverride: number | undefined;
|
|
let fgOverride: number | undefined;
|
|
|
|
// Apply decorations on the bottom layer
|
|
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
|
|
if (d.backgroundColorRGB) {
|
|
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
|
}
|
|
if (d.foregroundColorRGB) {
|
|
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
|
}
|
|
}
|
|
|
|
// Apply the selection color if needed
|
|
if (this._isCellSelected(x, y)) {
|
|
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
|
|
if (this._colors.selectionForeground) {
|
|
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
|
}
|
|
}
|
|
|
|
// Apply decorations on the top layer
|
|
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
|
|
if (d.backgroundColorRGB) {
|
|
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
|
}
|
|
if (d.foregroundColorRGB) {
|
|
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
|
}
|
|
}
|
|
|
|
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
|
|
// ahead of time in order to use the correct cache key
|
|
if (bgOverride !== undefined) {
|
|
// Non-RGB attributes from model + override + force RGB color mode
|
|
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
|
|
}
|
|
if (fgOverride !== undefined) {
|
|
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
|
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
|
|
}
|
|
|
|
// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
|
|
// resolving the other inverse color and setting the inverse flag if needed.
|
|
if (this._workColors.fg & FgFlags.INVERSE) {
|
|
if (bgOverride !== undefined && fgOverride === undefined) {
|
|
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
|
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
|
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
|
} else {
|
|
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
|
}
|
|
}
|
|
if (bgOverride === undefined && fgOverride !== undefined) {
|
|
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
|
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
|
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
|
} else {
|
|
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use the override if it exists
|
|
this._workColors.bg = bgOverride ?? this._workColors.bg;
|
|
this._workColors.fg = fgOverride ?? this._workColors.fg;
|
|
}
|
|
|
|
private _isCellSelected(x: number, y: number): boolean {
|
|
if (!this._model.selection.hasSelection) {
|
|
return false;
|
|
}
|
|
y -= this._terminal.buffer.active.viewportY;
|
|
if (this._model.selection.columnSelectMode) {
|
|
if (this._model.selection.startCol <= this._model.selection.endCol) {
|
|
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
|
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
|
}
|
|
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
|
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
|
}
|
|
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
|
|
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
|
|
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
|
|
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
|
|
}
|
|
|
|
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
|
const terminal = this._terminal;
|
|
|
|
// Selection does not exist
|
|
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
|
this._model.clearSelection();
|
|
return;
|
|
}
|
|
|
|
// Translate from buffer position to viewport position
|
|
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
|
|
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
|
|
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
|
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
|
|
|
// No need to draw the selection
|
|
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
|
this._model.clearSelection();
|
|
return;
|
|
}
|
|
|
|
this._model.selection.hasSelection = true;
|
|
this._model.selection.columnSelectMode = columnSelectMode;
|
|
this._model.selection.viewportStartRow = viewportStartRow;
|
|
this._model.selection.viewportEndRow = viewportEndRow;
|
|
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
|
|
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
|
this._model.selection.startCol = start[0];
|
|
this._model.selection.endCol = end[0];
|
|
}
|
|
|
|
/**
|
|
* Recalculates the character and canvas dimensions.
|
|
*/
|
|
private _updateDimensions(): void {
|
|
// TODO: Acquire CharSizeService properly
|
|
|
|
// Perform a new measure if the CharMeasure dimensions are not yet available
|
|
if (!(this._core as any)._charSizeService.width || !(this._core as any)._charSizeService.height) {
|
|
return;
|
|
}
|
|
|
|
// 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.
|
|
|
|
// NOTE: ceil fixes sometime, floor does others :s
|
|
|
|
this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._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._core as any)._charSizeService.height * this._devicePixelRatio);
|
|
|
|
// Calculate the scaled cell 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.scaledCellHeight = 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.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
|
|
|
// Calculate the scaled cell width, taking the letterSpacing into account.
|
|
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);
|
|
|
|
// Calculate the x coordinate with a cell that text should draw from in
|
|
// order to draw in the center of a cell.
|
|
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);
|
|
|
|
// Recalculate the canvas dimensions; scaled* define the actual number of
|
|
// pixel in the canvas
|
|
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
|
|
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
|
|
|
|
// 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 / this._devicePixelRatio);
|
|
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
|
|
|
|
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio;
|
|
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio;
|
|
|
|
// Get the _actual_ dimensions of an individual cell. This needs to be
|
|
// derived from the canvasWidth/Height calculated above which takes into
|
|
// account window.devicePixelRatio. CharMeasure.width/height by itself is
|
|
// insufficient when the page is not at 100% zoom level as CharMeasure is
|
|
// measured in CSS pixels, but the actual char size on the canvas can
|
|
// differ.
|
|
// this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
|
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
|
|
|
// This fixes 110% and 125%, not 150% or 175% though
|
|
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
|
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
|
}
|
|
|
|
private _requestRedrawViewport(): void {
|
|
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
|
}
|
|
}
|
|
|
|
// 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()];
|
|
}
|
|
}
|