Merge branch 'master' into buffer_optimizations

This commit is contained in:
Jörg Breitbart
2022-12-03 11:24:51 +01:00
85 changed files with 1502 additions and 1015 deletions
+2 -2
View File
@@ -274,7 +274,7 @@ export class AccessibilityManager extends Disposable {
}
private _refreshRowsDimensions(): void {
if (!this._renderService.dimensions.actualCellHeight) {
if (!this._renderService.dimensions.css.cell.height) {
return;
}
if (this._rowElements.length !== this._terminal.rows) {
@@ -286,7 +286,7 @@ export class AccessibilityManager extends Disposable {
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._renderService.dimensions.actualCellHeight}px`;
element.style.height = `${this._renderService.dimensions.css.cell.height}px`;
}
private _announceCharacters(): void {
-6
View File
@@ -39,8 +39,6 @@ export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionServ
/**
* Redirect the clipboard's data to the terminal's input handler.
* @param ev The original paste event to be handled
* @param term The terminal on which to apply the handled paste event
*/
export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, coreService: ICoreService): void {
ev.stopPropagation();
@@ -81,10 +79,6 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
/**
* Bind to right-click event and allow right-click copy and paste.
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
* @param selectionService The terminal's selection manager.
* @param shouldSelectWord If true and there is no selection the current word will be selected
*/
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, textarea, screenElement);
+3
View File
@@ -7,8 +7,11 @@ import { IDisposable } from 'common/Types';
/**
* Adds a disposable listener to a node in the DOM, returning the disposable.
* @param node The node to add a listener to.
* @param type The event type.
* @param handler The handler for the listener.
* @param options The boolean or options object to pass on to the event
* listener.
*/
export function addDisposableDomListener(
node: Element | Window | Document,
+5 -5
View File
@@ -9,6 +9,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { MockUnicodeService } from 'common/TestUtils.test';
import { IMarker } from 'common/Types';
import { ICoreService } from 'common/services/Services';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -47,11 +48,10 @@ describe('Terminal', () => {
});
describe('events', () => {
// TODO: Add an onData test back
// it('should fire the onData evnet', (done) => {
// term.onData(() => done());
// term.handler('fake');
// });
it('should fire the onData evnet', (done) => {
term.onData(() => done());
term.coreService.triggerDataEvent('fake');
});
it('should fire the onCursorMove event', () => {
return new Promise<void>(async r => {
term.onCursorMove(() => r());
+11 -12
View File
@@ -321,11 +321,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
return;
}
const cursorX = Math.min(this.buffer.x, this.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const cellHeight = this._renderService.dimensions.css.cell.height;
const width = bufferLine.getWidth(cursorX);
const cellWidth = this._renderService.dimensions.actualCellWidth * width;
const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
const cellWidth = this._renderService.dimensions.css.cell.width * width;
const cursorTop = this.buffer.y * this._renderService.dimensions.css.cell.height;
const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width;
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
@@ -979,10 +979,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
/**
* Handle a keydown event
* Key Resources:
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
* @param ev The keydown event to be handled.
* Handle a keydown [KeyboardEvent].
*
* [KeyboardEvent]: https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
*/
protected _keyDown(event: KeyboardEvent): boolean | undefined {
this._keyDownHandled = false;
@@ -1274,13 +1273,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
switch (type) {
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
const canvasWidth = this._renderService.dimensions.canvasWidth.toFixed(0);
const canvasHeight = this._renderService.dimensions.canvasHeight.toFixed(0);
const canvasWidth = this._renderService.dimensions.css.canvas.width.toFixed(0);
const canvasHeight = this._renderService.dimensions.css.canvas.height.toFixed(0);
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
break;
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
const cellWidth = this._renderService.dimensions.actualCellWidth.toFixed(0);
const cellHeight = this._renderService.dimensions.actualCellHeight.toFixed(0);
const cellWidth = this._renderService.dimensions.css.cell.width.toFixed(0);
const cellHeight = this._renderService.dimensions.css.cell.height.toFixed(0);
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
break;
}
+2 -14
View File
@@ -18,6 +18,7 @@ import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { css } from 'common/Color';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
@@ -372,20 +373,7 @@ export class MockRenderService implements IRenderService {
public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event;
public dimensions: IRenderDimensions = {
scaledCharWidth: 0,
scaledCharHeight: 0,
scaledCellWidth: 0,
scaledCellHeight: 0,
scaledCharLeft: 0,
scaledCharTop: 0,
scaledCanvasWidth: 0,
scaledCanvasHeight: 0,
canvasWidth: 0,
canvasHeight: 0,
actualCellWidth: 0,
actualCellHeight: 0
};
public dimensions: IRenderDimensions = createRenderDimensions();
public refreshRows(start: number, end: number): void {
throw new Error('Method not implemented.');
}
+6 -6
View File
@@ -26,7 +26,7 @@ interface ISmoothScrollState {
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _currentScaledCellHeight: number = 0;
private _currentDeviceCellHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0;
@@ -104,10 +104,10 @@ export class Viewport extends Disposable implements IViewport {
private _innerRefresh(): void {
if (this._charSizeService.height > 0) {
this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / this._coreBrowserService.dpr;
this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight;
this._currentRowHeight = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr;
this._currentDeviceCellHeight = this._renderService.dimensions.device.cell.height;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight);
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.css.canvas.height);
if (this._lastRecordedBufferHeight !== newBufferHeight) {
this._lastRecordedBufferHeight = newBufferHeight;
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
@@ -138,7 +138,7 @@ export class Viewport extends Disposable implements IViewport {
}
// If viewport height changed
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.css.canvas.height) {
this._refresh(immediate);
return;
}
@@ -150,7 +150,7 @@ export class Viewport extends Disposable implements IViewport {
}
// If row height changed
if (this._renderDimensions.scaledCellHeight !== this._currentScaledCellHeight) {
if (this._renderDimensions.device.cell.height !== this._currentDeviceCellHeight) {
this._refresh(immediate);
return;
}
@@ -28,7 +28,7 @@ export class BufferDecorationRenderer extends Disposable {
this._container.classList.add('xterm-decoration-container');
this._screenElement.appendChild(this._container);
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
this.register(this._renderService.onRenderedViewportChange(() => this._doRefreshDecorations()));
this.register(this._renderService.onDimensionsChange(() => {
this._dimensionsChanged = true;
this._queueRefresh();
@@ -50,12 +50,12 @@ export class BufferDecorationRenderer extends Disposable {
return;
}
this._animationFrame = this._renderService.addRefreshCallback(() => {
this.refreshDecorations();
this._doRefreshDecorations();
this._animationFrame = undefined;
});
}
public refreshDecorations(): void {
private _doRefreshDecorations(): void {
for (const decoration of this._decorationService.decorations) {
this._renderDecoration(decoration);
}
@@ -72,10 +72,10 @@ export class BufferDecorationRenderer extends Disposable {
private _createElement(decoration: IInternalDecoration): HTMLElement {
const element = document.createElement('div');
element.classList.add('xterm-decoration');
element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth)}px`;
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`;
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`;
element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`;
element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.css.cell.width)}px`;
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.css.cell.height}px`;
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.css.cell.height}px`;
element.style.lineHeight = `${this._renderService.dimensions.css.cell.height}px`;
const x = decoration.options.x ?? 0;
if (x && x > this._bufferService.cols) {
@@ -98,13 +98,12 @@ export class BufferDecorationRenderer extends Disposable {
} else {
let element = this._decorationElements.get(decoration);
if (!element) {
decoration.onDispose(() => this._removeDecoration(decoration));
element = this._createElement(decoration);
decoration.element = element;
this._decorationElements.set(decoration, element);
this._container.appendChild(element);
}
element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`;
element.style.top = `${line * this._renderService.dimensions.css.cell.height}px`;
element.style.display = this._altBufferIsActive ? 'none' : 'block';
decoration.onRenderEmitter.fire(element);
}
@@ -116,14 +115,15 @@ export class BufferDecorationRenderer extends Disposable {
}
const x = decoration.options.x ?? 0;
if ((decoration.options.anchor || 'left') === 'right') {
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
element.style.right = x ? `${x * this._renderService.dimensions.css.cell.width}px` : '';
} else {
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
element.style.left = x ? `${x * this._renderService.dimensions.css.cell.width}px` : '';
}
}
private _removeDecoration(decoration: IInternalDecoration): void {
this._decorationElements.get(decoration)?.remove();
this._decorationElements.delete(decoration);
decoration.dispose();
}
}
@@ -190,8 +190,6 @@ export class OverviewRulerRenderer extends Disposable {
}
private _renderColorZone(zone: IColorZone): void {
// TODO: Is _decorationElements needed?
this._ctx.fillStyle = zone.color;
this._ctx.fillRect(
/* x */ drawX[zone.position || 'full'],
+4 -4
View File
@@ -89,7 +89,7 @@ export class CompositionHelper {
/**
* Handles the keydown event, routing any necessary events to the CompositionHelper functions.
* @param ev The keydown event.
* @return Whether the Terminal should continue processing the keydown event.
* @returns Whether the Terminal should continue processing the keydown event.
*/
public keydown(ev: KeyboardEvent): boolean {
if (this._isComposing || this._isSendingComposition) {
@@ -218,9 +218,9 @@ export class CompositionHelper {
if (this._bufferService.buffer.isCursorInViewport) {
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
const cellHeight = this._renderService.dimensions.css.cell.height;
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.css.cell.height;
const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width;
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
+7 -3
View File
@@ -18,15 +18,19 @@ export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyl
* Gets coordinates within the terminal for a particular mouse event. The result
* is returned as an array in the form [x, y] instead of an object as it's a
* little faster and this function is used in some low level code.
* @param window The window object the element belongs to.
* @param event The mouse event.
* @param element The terminal's container element.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows n the terminal.
* @param hasValidCharSize Whether there is a valid character size available.
* @param cssCellWidth The cell width device pixel render dimensions.
* @param cssCellHeight The cell height device pixel render dimensions.
* @param isSelection Whether the request is for the selection or not. This will
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: Pick<MouseEvent, 'clientX' | 'clientY'>, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, cssCellWidth: number, cssCellHeight: number, isSelection?: boolean): [number, number] | undefined {
// Coordinates cannot be measured if there are no valid
if (!hasValidCharSize) {
return undefined;
@@ -37,8 +41,8 @@ export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clie
return undefined;
}
coords[0] = Math.ceil((coords[0] + (isSelection ? actualCellWidth / 2 : 0)) / actualCellWidth);
coords[1] = Math.ceil(coords[1] / actualCellHeight);
coords[0] = Math.ceil((coords[0] + (isSelection ? cssCellWidth / 2 : 0)) / cssCellWidth);
coords[1] = Math.ceil(coords[1] / cssCellHeight);
// Ensure coordinates are within the terminal viewport. Note that selections
// need an addition point of precision to cover the end point (as characters
+9 -9
View File
@@ -68,7 +68,7 @@ function resetStartingRow(startX: number, startY: number, targetX: number, targe
}
return repeat(bufferLine(
startX, startY, startX,
startY - wrappedRowsForRow(bufferService, startY), false, bufferService
startY - wrappedRowsForRow(startY, bufferService), false, bufferService
).length, sequence(Direction.LEFT, applicationCursor));
}
@@ -77,8 +77,8 @@ function resetStartingRow(startX: number, startY: number, targetX: number, targe
* ignoring wrapped rows
*/
function moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
const startRow = startY - wrappedRowsForRow(bufferService, startY);
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
const startRow = startY - wrappedRowsForRow(startY, bufferService);
const endRow = targetY - wrappedRowsForRow(targetY, bufferService);
const rowsToMove = Math.abs(startRow - endRow) - wrappedRowsCount(startY, targetY, bufferService);
@@ -91,7 +91,7 @@ function moveToRequestedRow(startY: number, targetY: number, bufferService: IBuf
function moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
let startRow;
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) {
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
startRow = targetY - wrappedRowsForRow(targetY, bufferService);
} else {
startRow = startY;
}
@@ -115,8 +115,8 @@ function moveToRequestedCol(startX: number, startY: number, targetX: number, tar
*/
function wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number {
let wrappedRows = 0;
const startRow = startY - wrappedRowsForRow(bufferService, startY);
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
const startRow = startY - wrappedRowsForRow(startY, bufferService);
const endRow = targetY - wrappedRowsForRow(targetY, bufferService);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1;
@@ -133,7 +133,7 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe
* Calculates the number of wrapped rows that make up a given row.
* @param currentRow The row to determine how many wrapped rows make it up
*/
function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number {
function wrappedRowsForRow(currentRow: number, bufferService: IBufferService): number {
let rowCount = 0;
let line = bufferService.buffer.lines.get(currentRow);
let lineWraps = line?.isWrapped;
@@ -157,7 +157,7 @@ function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): n
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
let startRow;
if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) {
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
startRow = targetY - wrappedRowsForRow(targetY, bufferService);
} else {
startRow = startY;
}
@@ -237,7 +237,7 @@ function sequence(direction: Direction, applicationCursor: boolean): string {
* Returns a string repeated a given number of times
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
* @param count The number of times to repeat the string
* @param string The string that is to be repeated
* @param str The string that is to be repeated
*/
function repeat(count: number, str: string): string {
count = Math.floor(count);
-3
View File
@@ -78,7 +78,6 @@ export class Terminal implements ITerminalApi {
public get element(): HTMLElement | undefined { return this._core.element; }
public get parser(): IParser {
this._checkProposedApi();
if (!this._parser) {
this._parser = new ParserApi(this._core);
}
@@ -92,7 +91,6 @@ export class Terminal implements ITerminalApi {
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
public get buffer(): IBufferNamespaceApi {
this._checkProposedApi();
if (!this._buffer) {
this._buffer = new BufferNamespaceApi(this._core);
}
@@ -148,7 +146,6 @@ export class Terminal implements ITerminalApi {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this._checkProposedApi();
return this._core.registerLinkProvider(linkProvider);
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
+42 -48
View File
@@ -3,16 +3,17 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'common/Color';
import { removeElementFromParent } from 'browser/Dom';
import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ILinkifier2, ILinkifierEvent, ReadonlyColorSet } from 'browser/Types';
import { color } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IInstantiationService, IOptionsService } from 'common/services/Services';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -64,20 +65,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this._selectionContainer.classList.add(SELECTION_CLASS);
this._selectionContainer.setAttribute('aria-hidden', 'true');
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.dimensions = createRenderDimensions();
this._updateDimensions();
this.register(this._optionsService.onOptionChange(() => this._handleOptionsChanged()));
@@ -104,23 +92,23 @@ export class DomRenderer extends Disposable implements IRenderer {
private _updateDimensions(): void {
const dpr = this._coreBrowserService.dpr;
this.dimensions.scaledCharWidth = this._charSizeService.width * dpr;
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
this.dimensions.scaledCharLeft = 0;
this.dimensions.scaledCharTop = 0;
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows;
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / dpr);
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / dpr);
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.device.char.width = this._charSizeService.width * dpr;
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
this.dimensions.device.char.left = 0;
this.dimensions.device.char.top = 0;
this.dimensions.device.canvas.width = this.dimensions.device.cell.width * this._bufferService.cols;
this.dimensions.device.canvas.height = this.dimensions.device.cell.height * this._bufferService.rows;
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr);
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr);
this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols;
this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows;
for (const element of this._rowElements) {
element.style.width = `${this.dimensions.canvasWidth}px`;
element.style.height = `${this.dimensions.actualCellHeight}px`;
element.style.lineHeight = `${this.dimensions.actualCellHeight}px`;
element.style.width = `${this.dimensions.css.canvas.width}px`;
element.style.height = `${this.dimensions.css.cell.height}px`;
element.style.lineHeight = `${this.dimensions.css.cell.height}px`;
// Make sure rows don't overflow onto following row
element.style.overflow = 'hidden';
}
@@ -135,14 +123,14 @@ export class DomRenderer extends Disposable implements IRenderer {
` display: inline-block;` +
` height: 100%;` +
` vertical-align: top;` +
` width: ${this.dimensions.actualCellWidth}px` +
` width: ${this.dimensions.css.cell.width}px` +
`}`;
this._dimensionsStyleElement.textContent = styles;
this._selectionContainer.style.height = this._viewportElement.style.height;
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
}
private _injectCss(colors: ReadonlyColorSet): void {
@@ -332,10 +320,10 @@ export class DomRenderer extends Disposable implements IRenderer {
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
const element = document.createElement('div');
element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`;
element.style.top = `${row * this.dimensions.actualCellHeight}px`;
element.style.left = `${colStart * this.dimensions.actualCellWidth}px`;
element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`;
element.style.height = `${rowCount * this.dimensions.css.cell.height}px`;
element.style.top = `${row * this.dimensions.css.cell.height}px`;
element.style.left = `${colStart * this.dimensions.css.cell.width}px`;
element.style.width = `${this.dimensions.css.cell.width * (colEnd - colStart)}px`;
return element;
}
@@ -350,7 +338,14 @@ export class DomRenderer extends Disposable implements IRenderer {
public clear(): void {
for (const e of this._rowElements) {
e.innerText = '';
/**
* NOTE: This used to be `e.innerText = '';` but that doesn't work when using `jsdom` and `@testing-library/react`
*
* references:
* - https://github.com/testing-library/react-testing-library/issues/1146
* - https://github.com/jsdom/jsdom/issues/1245
*/
e.replaceChildren();
}
}
@@ -361,11 +356,10 @@ export class DomRenderer extends Disposable implements IRenderer {
for (let y = start; y <= end; y++) {
const rowElement = this._rowElements[y];
rowElement.innerText = '';
const row = y + this._bufferService.buffer.ydisp;
const lineData = this._bufferService.buffer.lines.get(row);
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
rowElement.replaceChildren(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.css.cell.width, this._bufferService.cols));
}
}
@@ -28,13 +28,13 @@ const charAtlasCache: ITextureAtlasCacheEntry[] = [];
export function acquireTextureAtlas(
terminal: Terminal,
colors: ReadonlyColorSet,
scaledCellWidth: number,
scaledCellHeight: number,
scaledCharWidth: number,
scaledCharHeight: number,
deviceCellWidth: number,
deviceCellHeight: number,
deviceCharWidth: number,
deviceCharHeight: number,
devicePixelRatio: number
): ITextureAtlas {
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio);
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, terminal, colors, devicePixelRatio);
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
@@ -9,7 +9,7 @@ import { Terminal } from 'xterm';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { NULL_COLOR } from 'common/Color';
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig {
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, terminal: Terminal, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IColorSet = {
foreground: colors.foreground,
@@ -31,10 +31,10 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number
devicePixelRatio,
letterSpacing: terminal.options.letterSpacing,
lineHeight: terminal.options.lineHeight,
scaledCellWidth,
scaledCellHeight,
scaledCharWidth,
scaledCharHeight,
deviceCellWidth: deviceCellWidth,
deviceCellHeight: deviceCellHeight,
deviceCharWidth: deviceCharWidth,
deviceCharHeight: deviceCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
@@ -61,8 +61,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.deviceCharWidth === b.deviceCharWidth &&
a.deviceCharHeight === b.deviceCharHeight &&
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
a.minimumContrastRatio === b.minimumContrastRatio &&
a.colors.foreground.rgba === b.colors.foreground.rgba &&
+21 -21
View File
@@ -381,32 +381,32 @@ export function tryDrawCustomChar(
c: string,
xOffset: number,
yOffset: number,
scaledCellWidth: number,
scaledCellHeight: number,
deviceCellWidth: number,
deviceCellHeight: number,
fontSize: number,
devicePixelRatio: number
): boolean {
const blockElementDefinition = blockElementDefinitions[c];
if (blockElementDefinition) {
drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
}
const patternDefinition = patternCharacterDefinitions[c];
if (patternDefinition) {
drawPatternChar(ctx, patternDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
drawPatternChar(ctx, patternDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
}
const boxDrawingDefinition = boxDrawingDefinitions[c];
if (boxDrawingDefinition) {
drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, devicePixelRatio);
drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio);
return true;
}
const powerlineDefinition = powerlineDefinitions[c];
if (powerlineDefinition) {
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, fontSize, devicePixelRatio);
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
return true;
}
@@ -418,13 +418,13 @@ function drawBlockElementChar(
charDefinition: IBlockVector[],
xOffset: number,
yOffset: number,
scaledCellWidth: number,
scaledCellHeight: number
deviceCellWidth: number,
deviceCellHeight: number
): void {
for (let i = 0; i < charDefinition.length; i++) {
const box = charDefinition[i];
const xEighth = scaledCellWidth / 8;
const yEighth = scaledCellHeight / 8;
const xEighth = deviceCellWidth / 8;
const yEighth = deviceCellHeight / 8;
ctx.fillRect(
xOffset + box.x * xEighth,
yOffset + box.y * yEighth,
@@ -441,8 +441,8 @@ function drawPatternChar(
charDefinition: number[][],
xOffset: number,
yOffset: number,
scaledCellWidth: number,
scaledCellHeight: number
deviceCellWidth: number,
deviceCellHeight: number
): void {
let patternSet = cachedPatterns.get(charDefinition);
if (!patternSet) {
@@ -492,7 +492,7 @@ function drawPatternChar(
patternSet.set(fillStyle, pattern);
}
ctx.fillStyle = pattern;
ctx.fillRect(xOffset, yOffset, scaledCellWidth, scaledCellHeight);
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
}
/**
@@ -540,8 +540,8 @@ function drawBoxDrawingChar(
charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
xOffset: number,
yOffset: number,
scaledCellWidth: number,
scaledCellHeight: number,
deviceCellWidth: number,
deviceCellHeight: number,
devicePixelRatio: number
): void {
ctx.strokeStyle = ctx.fillStyle;
@@ -551,7 +551,7 @@ function drawBoxDrawingChar(
let actualInstructions: string;
if (typeof instructions === 'function') {
const xp = .15;
const yp = .15 / scaledCellHeight * scaledCellWidth;
const yp = .15 / deviceCellHeight * deviceCellWidth;
actualInstructions = instructions(xp, yp);
} else {
actualInstructions = instructions;
@@ -567,7 +567,7 @@ function drawBoxDrawingChar(
if (!args[0] || !args[1]) {
continue;
}
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, true, devicePixelRatio));
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio));
}
ctx.stroke();
ctx.closePath();
@@ -579,8 +579,8 @@ function drawPowerlineChar(
charDefinition: IVectorShape,
xOffset: number,
yOffset: number,
scaledCellWidth: number,
scaledCellHeight: number,
deviceCellWidth: number,
deviceCellHeight: number,
fontSize: number,
devicePixelRatio: number
): void {
@@ -601,8 +601,8 @@ function drawPowerlineChar(
}
f(ctx, translateArgs(
args,
scaledCellWidth,
scaledCellHeight,
deviceCellWidth,
deviceCellHeight,
xOffset,
yOffset,
false,
@@ -3,6 +3,8 @@
* @license MIT
*/
import { IDimensions, IOffset, IRenderDimensions } from 'browser/renderer/shared/Types';
export function throwIfFalsy<T>(value: T | undefined | null): T {
if (!value) {
throw new Error('value must not be falsy');
@@ -28,3 +30,29 @@ function isBoxOrBlockGlyph(codepoint: number): boolean {
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
}
export function createRenderDimensions(): IRenderDimensions {
return {
css: {
canvas: createDimension(),
cell: createDimension()
},
device: {
canvas: createDimension(),
cell: createDimension(),
char: {
width: 0,
height: 0,
left: 0,
top: 0
}
}
};
}
function createDimension(): IDimensions {
return {
width: 0,
height: 0
};
}
+354 -149
View File
@@ -13,32 +13,37 @@ import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlin
import { IUnicodeService } from 'common/services/Services';
import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
import { EventEmitter } from 'common/EventEmitter';
// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;
/**
* The amount of the texture to be filled before throwing it away and starting
* again. Since the throw away and individual glyph draws don't cost too much,
* this prevent juggling multiple textures in the GL context.
*/
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
/**
* A shared object which is used to draw nothing for a particular cell.
*/
const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
offset: { x: 0, y: 0 },
texturePage: 0,
texturePosition: { x: 0, y: 0 },
texturePositionClipSpace: { x: 0, y: 0 },
offset: { x: 0, y: 0 },
size: { x: 0, y: 0 },
sizeClipSpace: { x: 0, y: 0 }
};
const TMP_CANVAS_GLYPH_PADDING = 2;
const enum Constants {
/**
* The amount of pixel padding to allow in each row. Setting this to zero would make the atlas
* page pack as tightly as possible, but more pages would end up being created as a result.
*/
ROW_PIXEL_THRESHOLD = 2,
/**
* The maximum texture size regardless of what the actual hardware maximum turns out to be. This
* is enforced to ensure uploading the texture still finishes in a reasonable amount of time. A
* 4096 squared image takes up 16MB of GPU memory.
*/
FORCED_MAX_TEXTURE_SIZE = 4096
}
interface ICharAtlasActiveRow {
x: number;
y: number;
@@ -55,51 +60,40 @@ export class TextureAtlas implements ITextureAtlas {
private _cacheMapCombined: FourKeyMap<string, number, number, number, IRasterizedGlyph> = new FourKeyMap();
// The texture that the atlas is drawn to
public cacheCanvas: HTMLCanvasElement;
private _cacheCtx: CanvasRenderingContext2D;
private _pages: AtlasPage[] = [];
public get pages(): { canvas: HTMLCanvasElement, version: number }[] { return this._pages; }
// The set of atlas pages that can be written to
private _activePages: AtlasPage[] = [];
private _tmpCanvas: HTMLCanvasElement;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
// Texture atlas current positioning data. The texture packing strategy used is to fill from
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
// row's height, the following happens:
//
// - The current row becomes the fixed height row A
// - A new fixed height row B the exact size of the glyph is created below the current row
// - A new dynamic height current row is created below B
//
// This strategy does a good job preventing space being wasted for very short glyphs such as
// underscores, hyphens etc. or those with underlines rendered.
private _currentRow: ICharAtlasActiveRow = {
x: 0,
y: 0,
height: 0
};
private readonly _fixedRows: ICharAtlasActiveRow[] = [];
public hasCanvasChanged = false;
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
private _workAttributeData: AttributeData = new AttributeData();
private _textureSize: number = 512;
public static maxAtlasPages: number | undefined;
public static maxTextureSize: number | undefined;
private readonly _onAddTextureAtlasCanvas = new EventEmitter<HTMLCanvasElement>();
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
private readonly _onRemoveTextureAtlasCanvas = new EventEmitter<HTMLCanvasElement>();
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
constructor(
document: Document,
private readonly _document: Document,
private readonly _config: ICharAtlasConfig,
private readonly _unicodeService: IUnicodeService
) {
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = TEXTURE_WIDTH;
this.cacheCanvas.height = TEXTURE_HEIGHT;
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
this._tmpCanvas = document.createElement('canvas');
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
this._createNewPage();
this._tmpCanvas = createCanvas(
_document,
this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2,
this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2
);
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {
alpha: this._config.allowTransparency,
willReadFrequently: true
@@ -107,9 +101,10 @@ export class TextureAtlas implements ITextureAtlas {
}
public dispose(): void {
if (this.cacheCanvas.parentElement) {
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
for (const page of this.pages) {
page.canvas.remove();
}
this._onAddTextureAtlasCanvas.dispose();
}
public warmUp(): void {
@@ -132,28 +127,119 @@ export class TextureAtlas implements ITextureAtlas {
}
}
private _requestClearModel = false;
public beginFrame(): boolean {
if (this._currentRow.y > TEXTURE_CAPACITY) {
this.clearTexture();
this.warmUp();
return true;
}
return false;
return this._requestClearModel;
}
public clearTexture(): void {
if (this._currentRow.x === 0 && this._currentRow.y === 0) {
if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
return;
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
for (const page of this._pages) {
page.clear();
}
this._cacheMap.clear();
this._cacheMapCombined.clear();
this._currentRow.x = 0;
this._currentRow.y = 0;
this._currentRow.height = 0;
this._fixedRows.length = 0;
this._didWarmUp = false;
this.hasCanvasChanged = true;
}
private _createNewPage(): AtlasPage {
// Try merge the set of the 4 most used pages of the largest size. This is is deferred to a
// microtask to ensure it does not interrupt textures that will be rendered in the current
// animation frame which would result in blank rendered areas. This is actually not that
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
if (TextureAtlas.maxAtlasPages && this._pages.length >= Math.max(4, TextureAtlas.maxAtlasPages / 2)) {
queueMicrotask(() => {
// Find the set of the largest 4 images, below the maximum size, with the highest
// percentages used
const pagesBySize = this._pages.filter(e => {
return e.canvas.width * 2 <= (TextureAtlas.maxTextureSize || Constants.FORCED_MAX_TEXTURE_SIZE);
}).sort((a, b) => {
if (b.canvas.width !== a.canvas.width) {
return b.canvas.width - a.canvas.width;
}
return b.percentageUsed - a.percentageUsed;
});
let sameSizeI = -1;
let size = 0;
for (let i = 0; i < pagesBySize.length; i++) {
if (pagesBySize[i].canvas.width !== size) {
sameSizeI = i;
size = pagesBySize[i].canvas.width;
} else if (i - sameSizeI === 3) {
break;
}
}
// Gather details of the merge
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
const mergedPageIndex = sortedMergingPagesIndexes[0];
// Merge into the new page
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
mergedPage.version++;
// Replace the first _merging_ page with the _merged_ page
this._pages[mergedPageIndex] = mergedPage;
// Delete the other 3 pages, shifting glyph texture pages as needed
for (let i = sortedMergingPagesIndexes.length - 1; i >= 1; i--) {
this._deletePage(sortedMergingPagesIndexes[i]);
}
// Request the model to be cleared to refresh all texture pages.
this._requestClearModel = true;
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
});
}
// All new atlas pages are created small as they are highly dynamic
const newPage = new AtlasPage(this._document, this._textureSize);
this._pages.push(newPage);
this._activePages.push(newPage);
this._onAddTextureAtlasCanvas.fire(newPage.canvas);
return newPage;
}
private _mergePages(mergingPages: AtlasPage[], mergedPageIndex: number): AtlasPage {
const mergedSize = mergingPages[0].canvas.width * 2;
const mergedPage = new AtlasPage(this._document, mergedSize, mergingPages);
for (const [i, p] of mergingPages.entries()) {
const xOffset = i * p.canvas.width % mergedSize;
const yOffset = Math.floor(i / 2) * p.canvas.height;
mergedPage.ctx.drawImage(p.canvas, xOffset, yOffset);
for (const g of p.glyphs) {
g.texturePage = mergedPageIndex;
g.sizeClipSpace.x = g.size.x / mergedSize;
g.sizeClipSpace.y = g.size.y / mergedSize;
g.texturePosition.x += xOffset;
g.texturePosition.y += yOffset;
g.texturePositionClipSpace.x = g.texturePosition.x / mergedSize;
g.texturePositionClipSpace.y = g.texturePosition.y / mergedSize;
}
this._onRemoveTextureAtlasCanvas.fire(p.canvas);
// Remove the merging page from active pages if it was there
const index = this._activePages.indexOf(p);
if (index !== -1) {
this._activePages.splice(index, 1);
}
}
return mergedPage;
}
private _deletePage(pageIndex: number): void {
this._pages.splice(pageIndex, 1);
for (let j = pageIndex; j < this._pages.length; j++) {
const adjustingPage = this._pages[j];
for (const g of adjustingPage.glyphs) {
g.texturePage--;
}
adjustingPage.version++;
}
}
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph {
@@ -340,17 +426,15 @@ export class TextureAtlas implements ITextureAtlas {
// Uncomment for debugging
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
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.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
const allowedWidth = this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
}
// Include line height when drawing glyphs
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 4;
const allowedHeight = this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 4;
if (this._tmpCanvas.height < allowedHeight) {
this._tmpCanvas.height = allowedHeight;
}
@@ -411,7 +495,7 @@ export class TextureAtlas implements ITextureAtlas {
// Draw custom characters if applicable
let customGlyph = false;
if (this._config.customGlyphs !== false) {
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight, this._config.fontSize, this._config.devicePixelRatio);
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.fontSize, this._config.devicePixelRatio);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
@@ -452,15 +536,15 @@ export class TextureAtlas implements ITextureAtlas {
// Underline style/stroke
this._tmpCtx.beginPath();
const xLeft = padding;
const yTop = Math.ceil(padding + this._config.scaledCharHeight) - yOffset;
const yMid = padding + this._config.scaledCharHeight + lineWidth - yOffset;
const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth * 2) - yOffset;
const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset;
const yMid = padding + this._config.deviceCharHeight + lineWidth - yOffset;
const yBot = Math.ceil(padding + this._config.deviceCharHeight + lineWidth * 2) - yOffset;
for (let i = 0; i < chWidth; i++) {
this._tmpCtx.save();
const xChLeft = xLeft + i * this._config.scaledCellWidth;
const xChRight = xLeft + (i + 1) * this._config.scaledCellWidth;
const xChMid = xChLeft + this._config.scaledCellWidth / 2;
const xChLeft = xLeft + i * this._config.deviceCellWidth;
const xChRight = xLeft + (i + 1) * this._config.deviceCellWidth;
const xChMid = xChLeft + this._config.deviceCellWidth / 2;
switch (this._workAttributeData.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
this._tmpCtx.moveTo(xChLeft, yTop);
@@ -471,18 +555,18 @@ export class TextureAtlas implements ITextureAtlas {
case UnderlineStyle.CURLY:
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
// made taller when the line width is as otherwise it's not very clear otherwise.
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.deviceCharHeight - lineWidth / 2) - yOffset;
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.deviceCharHeight + lineWidth / 2) - yOffset;
// Clip the left and right edges of the underline such that it can be drawn just outside
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
// glyphs adjacent to one another.
const clipRegion = new Path2D();
clipRegion.rect(xChLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, yBot - yTop);
this._tmpCtx.clip(clipRegion);
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
this._tmpCtx.moveTo(xChLeft - this._config.scaledCellWidth / 2, yMid);
this._tmpCtx.moveTo(xChLeft - this._config.deviceCellWidth / 2, yMid);
this._tmpCtx.bezierCurveTo(
xChLeft - this._config.scaledCellWidth / 2, yCurlyTop,
xChLeft - this._config.deviceCellWidth / 2, yCurlyTop,
xChLeft, yCurlyTop,
xChLeft, yMid
);
@@ -498,8 +582,8 @@ export class TextureAtlas implements ITextureAtlas {
);
this._tmpCtx.bezierCurveTo(
xChRight, yCurlyBot,
xChRight + this._config.scaledCellWidth / 2, yCurlyBot,
xChRight + this._config.scaledCellWidth / 2, yMid
xChRight + this._config.deviceCellWidth / 2, yCurlyBot,
xChRight + this._config.deviceCellWidth / 2, yMid
);
break;
case UnderlineStyle.DOTTED:
@@ -543,11 +627,11 @@ export class TextureAtlas implements ITextureAtlas {
// outline around the whole glyph, as well as additional pixels in the glyph at the top
// which would increase GPU memory demands
const clipRegion = new Path2D();
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.scaledCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.deviceCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
this._tmpCtx.clip(clipRegion);
this._tmpCtx.lineWidth = this._config.devicePixelRatio * 3;
this._tmpCtx.strokeStyle = backgroundColor.css;
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
this._tmpCtx.strokeText(chars, padding, padding + this._config.deviceCharHeight);
this._tmpCtx.restore();
}
}
@@ -556,21 +640,21 @@ export class TextureAtlas implements ITextureAtlas {
// Draw the character
if (!customGlyph) {
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight);
}
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible
// even on the bottom row, try for a maximum of 5 pixels.
if (chars === '_' && !this._config.allowTransparency) {
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
if (isBeyondCellBounds) {
for (let offset = 1; offset <= 5; offset++) {
this._tmpCtx.save();
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
this._tmpCtx.restore();
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight - offset);
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
if (!isBeyondCellBounds) {
break;
}
@@ -585,8 +669,8 @@ export class TextureAtlas implements ITextureAtlas {
this._tmpCtx.lineWidth = lineWidth;
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
this._tmpCtx.beginPath();
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth * chWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.deviceCharHeight / 2) - yOffset);
this._tmpCtx.lineTo(padding + this._config.deviceCharWidth * chWidth, padding + Math.floor(this._config.deviceCharHeight / 2) - yOffset);
this._tmpCtx.stroke();
}
@@ -612,63 +696,119 @@ export class TextureAtlas implements ITextureAtlas {
}
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, restrictedPowerlineGlyph, customGlyph, padding);
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
// Find the best atlas row to use
let activePage: AtlasPage;
let activeRow: ICharAtlasActiveRow;
while (true) {
// Select the ideal existing row, preferring fixed rows over the current row
activeRow = this._currentRow;
for (const row of this._fixedRows) {
if ((activeRow === this._currentRow || row.height < activeRow.height) && rasterizedGlyph.size.y <= row.height) {
activeRow = row;
}
}
// Create a new one if vertical space would be wasted, fixing the previously active row in the
// process as it now has a fixed height
if (activeRow.height > rasterizedGlyph.size.y * 2) {
// Fix the current row as the new row is being added below
if (this._currentRow.height > 0) {
this._fixedRows.push(this._currentRow);
}
// Create the new fixed height row
activeRow = {
x: 0,
y: this._currentRow.y + this._currentRow.height,
height: rasterizedGlyph.size.y
};
this._fixedRows.push(activeRow);
// Create the new current row below the new fixed height row
this._currentRow = {
x: 0,
y: activeRow.y + activeRow.height,
height: 0
};
}
// Exit the loop if there is enough room in the row
if (activeRow.x + rasterizedGlyph.size.x <= TEXTURE_WIDTH) {
// If there are no active pages (the last smallest 4 were merged), create a new one
if (this._activePages.length === 0) {
const newPage = this._createNewPage();
activePage = newPage;
activeRow = newPage.currentRow;
activeRow.height = rasterizedGlyph.size.y;
break;
}
// If there is enough room in the current row, finish it and try again
if (activeRow === this._currentRow) {
// Get the best current row from all active pages
activePage = this._activePages[this._activePages.length - 1];
activeRow = activePage.currentRow;
for (const p of this._activePages) {
if (rasterizedGlyph.size.y <= p.currentRow.height) {
activePage = p;
activeRow = p.currentRow;
}
}
// TODO: This algorithm could be simplified:
// - Search for the page with ROW_PIXEL_THRESHOLD in mind
// - Keep track of current/fixed rows in a Map
// Replace the best current row with a fixed row if there is one at least as good as the
// current row. Search in reverse to prioritize filling in older pages.
for (let i = this._activePages.length - 1; i >= 0; i--) {
for (const row of this._activePages[i].fixedRows) {
if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) {
activePage = this._activePages[i];
activeRow = row;
}
}
}
// Create a new one if too much vertical space would be wasted or there is not enough room
// left in the page. The previous active row will become fixed in the process as it now has a
// fixed height
if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) {
// Create the new fixed height row, creating a new page if there isn't enough room on the
// current page
let wasNewPageCreated = false;
if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) {
// Find the first page with room to create the new row on
let candidatePage: AtlasPage | undefined;
for (const p of this._activePages) {
if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) {
candidatePage = p;
break;
}
}
if (candidatePage) {
activePage = candidatePage;
} else {
// Create a new page if there is no room
const newPage = this._createNewPage();
activePage = newPage;
activeRow = newPage.currentRow;
activeRow.height = rasterizedGlyph.size.y;
wasNewPageCreated = true;
}
}
if (!wasNewPageCreated) {
// Fix the current row as the new row is being added below
if (activePage.currentRow.height > 0) {
activePage.fixedRows.push(activePage.currentRow);
}
activeRow = {
x: 0,
y: activePage.currentRow.y + activePage.currentRow.height,
height: rasterizedGlyph.size.y
};
activePage.fixedRows.push(activeRow);
// Create the new current row below the new fixed height row
activePage.currentRow = {
x: 0,
y: activeRow.y + activeRow.height,
height: 0
};
}
// TODO: Remove pages from _activePages when all rows are filled
}
// Exit the loop if there is enough room in the row
if (activeRow.x + rasterizedGlyph.size.x <= activePage.canvas.width) {
break;
}
// If there is not enough room in the current row, finish it and try again
if (activeRow === activePage.currentRow) {
activeRow.x = 0;
activeRow.y += activeRow.height;
activeRow.height = 0;
} else {
this._fixedRows.splice(this._fixedRows.indexOf(activeRow), 1);
activePage.fixedRows.splice(activePage.fixedRows.indexOf(activeRow), 1);
}
}
// Record texture position
rasterizedGlyph.texturePage = this._pages.indexOf(activePage);
rasterizedGlyph.texturePosition.x = activeRow.x;
rasterizedGlyph.texturePosition.y = activeRow.y;
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH;
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / TEXTURE_HEIGHT;
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / activePage.canvas.width;
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / activePage.canvas.height;
// Fix the clipspace position as pages may be of differing size
rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width;
rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height;
// Update atlas current row, for fixed rows the glyph height will never be larger than the row
// height
@@ -676,7 +816,17 @@ export class TextureAtlas implements ITextureAtlas {
activeRow.x += rasterizedGlyph.size.x;
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y);
activePage.ctx.putImageData(
imageData,
rasterizedGlyph.texturePosition.x - this._workBoundingBox.left,
rasterizedGlyph.texturePosition.y - this._workBoundingBox.top,
this._workBoundingBox.left,
this._workBoundingBox.top,
rasterizedGlyph.size.x,
rasterizedGlyph.size.y
);
activePage.addGlyph(rasterizedGlyph);
activePage.version++;
return rasterizedGlyph;
}
@@ -690,8 +840,8 @@ export class TextureAtlas implements ITextureAtlas {
*/
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean, padding: number): IRasterizedGlyph {
boundingBox.top = 0;
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
const width = restrictedGlyph ? this._config.scaledCellWidth : allowedWidth;
const height = restrictedGlyph ? this._config.deviceCellHeight : this._tmpCanvas.height;
const width = restrictedGlyph ? this._config.deviceCellWidth : allowedWidth;
let found = false;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
@@ -708,7 +858,7 @@ export class TextureAtlas implements ITextureAtlas {
}
boundingBox.left = 0;
found = false;
for (let x = 0; x < width; x++) {
for (let x = 0; x < padding + width; x++) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
@@ -723,7 +873,7 @@ export class TextureAtlas implements ITextureAtlas {
}
boundingBox.right = width;
found = false;
for (let x = width - 1; x >= 0; x--) {
for (let x = padding + width - 1; x >= padding; x--) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
@@ -752,6 +902,7 @@ export class TextureAtlas implements ITextureAtlas {
}
}
return {
texturePage: 0,
texturePosition: { x: 0, y: 0 },
texturePositionClipSpace: { x: 0, y: 0 },
size: {
@@ -759,31 +910,78 @@ export class TextureAtlas implements ITextureAtlas {
y: boundingBox.bottom - boundingBox.top + 1
},
sizeClipSpace: {
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
x: (boundingBox.right - boundingBox.left + 1),
y: (boundingBox.bottom - boundingBox.top + 1)
},
offset: {
x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.scaledCellWidth - this._config.scaledCharWidth) / 2) : 0),
y: -boundingBox.top + padding + ((restrictedGlyph || customGlyph) ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.deviceCellWidth - this._config.deviceCharWidth) / 2) : 0),
y: -boundingBox.top + padding + ((restrictedGlyph || customGlyph) ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.deviceCellHeight - this._config.deviceCharHeight) / 2) : 0)
}
};
}
}
private _clipImageData(imageData: ImageData, boundingBox: IBoundingBox): ImageData {
const width = boundingBox.right - boundingBox.left + 1;
const height = boundingBox.bottom - boundingBox.top + 1;
const clippedData = new Uint8ClampedArray(width * height * 4);
for (let y = boundingBox.top; y <= boundingBox.bottom; y++) {
for (let x = boundingBox.left; x <= boundingBox.right; x++) {
const oldOffset = y * this._tmpCanvas.width * 4 + x * 4;
const newOffset = (y - boundingBox.top) * width * 4 + (x - boundingBox.left) * 4;
clippedData[newOffset] = imageData.data[oldOffset];
clippedData[newOffset + 1] = imageData.data[oldOffset + 1];
clippedData[newOffset + 2] = imageData.data[oldOffset + 2];
clippedData[newOffset + 3] = imageData.data[oldOffset + 3];
class AtlasPage {
public readonly canvas: HTMLCanvasElement;
public readonly ctx: CanvasRenderingContext2D;
private _usedPixels: number = 0;
public get percentageUsed(): number { return this._usedPixels / (this.canvas.width * this.canvas.height); }
private readonly _glyphs: IRasterizedGlyph[] = [];
public get glyphs(): ReadonlyArray<IRasterizedGlyph> { return this._glyphs; }
public addGlyph(glyph: IRasterizedGlyph): void {
this._glyphs.push(glyph);
this._usedPixels += glyph.size.x * glyph.size.y;
}
/**
* Used to check whether the canvas of the atlas page has changed.
*/
public version = 0;
// Texture atlas current positioning data. The texture packing strategy used is to fill from
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
// row's height, the following happens:
//
// - The current row becomes the fixed height row A
// - A new fixed height row B the exact size of the glyph is created below the current row
// - A new dynamic height current row is created below B
//
// This strategy does a good job preventing space being wasted for very short glyphs such as
// underscores, hyphens etc. or those with underlines rendered.
public currentRow: ICharAtlasActiveRow = {
x: 0,
y: 0,
height: 0
};
public readonly fixedRows: ICharAtlasActiveRow[] = [];
constructor(
document: Document,
size: number,
sourcePages?: AtlasPage[]
) {
if (sourcePages) {
for (const p of sourcePages) {
this._glyphs.push(...p.glyphs);
this._usedPixels += p._usedPixels;
}
}
return new ImageData(clippedData, width, height);
this.canvas = createCanvas(document, size, size);
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
}
public clear(): void {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.currentRow.x = 0;
this.currentRow.y = 0;
this.currentRow.height = 0;
this.fixedRows.length = 0;
this.version++;
}
}
@@ -841,3 +1039,10 @@ function checkCompletelyTransparent(imageData: ImageData): boolean {
}
return true;
}
function createCanvas(document: Document, width: number, height: number): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}
+38 -19
View File
@@ -4,7 +4,7 @@
*/
import { FontWeight, Terminal } from 'xterm';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IColorSet } from 'browser/Types';
import { IDisposable } from 'common/Types';
import { IEvent } from 'common/EventEmitter';
@@ -17,29 +17,42 @@ export interface ICharAtlasConfig {
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharWidth: number;
scaledCharHeight: number;
deviceCellWidth: number;
deviceCellHeight: number;
deviceCharWidth: number;
deviceCharHeight: number;
allowTransparency: boolean;
drawBoldTextInBrightColors: boolean;
minimumContrastRatio: number;
colors: IColorSet;
}
export interface IDimensions {
width: number;
height: number;
}
export interface IOffset {
top: number;
left: number;
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
/**
* Dimensions measured in CSS pixels (ie. device pixels / device pixel ratio).
*/
css: {
canvas: IDimensions;
cell: IDimensions;
};
/**
* Dimensions measured in actual pixels as rendered to the device.
*/
device: {
canvas: IDimensions;
cell: IDimensions;
char: IDimensions & IOffset;
};
}
export interface IRequestRedrawEvent {
@@ -74,8 +87,10 @@ export interface IRenderer extends IDisposable {
}
export interface ITextureAtlas extends IDisposable {
readonly cacheCanvas: HTMLCanvasElement;
hasCanvasChanged: boolean;
readonly pages: { canvas: HTMLCanvasElement, version: number }[];
onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
/**
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
@@ -107,6 +122,10 @@ export interface IRasterizedGlyph {
* in pixels.
*/
offset: IVector;
/**
* The index of the texture page that the glyph is on.
*/
texturePage: number;
/**
* the x and y position of the glyph in the texture in pixels.
*/
+1 -1
View File
@@ -118,7 +118,7 @@ export class SelectionModel {
/**
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
* @return Whether a refresh is necessary.
* @returns Whether a refresh is necessary.
*/
public handleTrim(amount: number): boolean {
// Adjust the selection position based on the trimmed amount.
+10 -2
View File
@@ -8,10 +8,15 @@ import { ICoreBrowserService } from './Services';
export class CoreBrowserService implements ICoreBrowserService {
public serviceBrand: undefined;
private _isFocused = false;
private _cachedIsFocused: boolean | undefined = undefined;
constructor(
private _textarea: HTMLTextAreaElement,
public readonly window: Window & typeof globalThis
) {
this._textarea.addEventListener('focus', () => this._isFocused = true);
this._textarea.addEventListener('blur', () => this._isFocused = false);
}
public get dpr(): number {
@@ -19,7 +24,10 @@ export class CoreBrowserService implements ICoreBrowserService {
}
public get isFocused(): boolean {
const docOrShadowRoot = this._textarea.getRootNode ? this._textarea.getRootNode() as Document | ShadowRoot : this._textarea.ownerDocument;
return docOrShadowRoot.activeElement === this._textarea && this._textarea.ownerDocument.hasFocus();
if (this._cachedIsFocused === undefined) {
this._cachedIsFocused = this._isFocused && this._textarea.ownerDocument.hasFocus();
queueMicrotask(() => this._cachedIsFocused = undefined);
}
return this._cachedIsFocused;
}
}
+6 -6
View File
@@ -23,8 +23,8 @@ export class MouseService implements IMouseService {
colCount,
rowCount,
this._charSizeService.hasValidSize,
this._renderService.dimensions.actualCellWidth,
this._renderService.dimensions.actualCellHeight,
this._renderService.dimensions.css.cell.width,
this._renderService.dimensions.css.cell.height,
isSelection
);
}
@@ -37,14 +37,14 @@ export class MouseService implements IMouseService {
if (!this._charSizeService.hasValidSize
|| coords[0] < 0
|| coords[1] < 0
|| coords[0] >= this._renderService.dimensions.canvasWidth
|| coords[1] >= this._renderService.dimensions.canvasHeight) {
|| coords[0] >= this._renderService.dimensions.css.canvas.width
|| coords[1] >= this._renderService.dimensions.css.canvas.height) {
return undefined;
}
return {
col: Math.floor(coords[0] / this._renderService.dimensions.actualCellWidth),
row: Math.floor(coords[1] / this._renderService.dimensions.actualCellHeight),
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
x: Math.floor(coords[0]),
y: Math.floor(coords[1])
};
+1 -1
View File
@@ -186,7 +186,7 @@ export class RenderService extends Disposable implements IRenderService {
return;
}
// Don't fire the event if the dimensions haven't changed
if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) {
if (this._renderer.dimensions.css.canvas.width === this._canvasWidth && this._renderer.dimensions.css.canvas.height === this._canvasHeight) {
return;
}
this._onDimensionsChange.fire(this._renderer.dimensions);
@@ -49,8 +49,8 @@ describe('SelectionService', () => {
bufferService = new MockBufferService(20, 20, optionsService);
buffer = bufferService.buffer;
const renderService = new MockRenderService();
renderService.dimensions.canvasHeight = 10 * 20;
renderService.dimensions.canvasWidth = 10 * 20;
renderService.dimensions.css.canvas.height = 10 * 20;
renderService.dimensions.css.canvas.width = 10 * 20;
selectionService = new TestSelectionService(bufferService, optionsService, renderService);
});
+11 -10
View File
@@ -408,7 +408,7 @@ export class SelectionService extends Disposable implements ISelectionService {
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = getCoordsRelativeToElement(this._coreBrowserService.window, event, this._screenElement)[1];
const terminalHeight = this._renderService.dimensions.canvasHeight;
const terminalHeight = this._renderService.dimensions.css.canvas.height;
if (offset >= 0 && offset <= terminalHeight) {
return 0;
}
@@ -757,19 +757,20 @@ export class SelectionService extends Disposable implements ISelectionService {
}
/**
* Converts a viewport column to the character index on the buffer line, the
* latter takes into account wide characters.
* @param coords The coordinates to find the 2 index for.
* Converts a viewport column (0 to cols - 1) to the character index on the
* buffer line, the latter takes into account wide and null characters.
* @param bufferLine The buffer line to use.
* @param x The x index in the buffer line to convert.
*/
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
let charIndex = coords[0];
for (let i = 0; coords[0] >= i; i++) {
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, x: number): number {
let charIndex = x;
for (let i = 0; x >= i; i++) {
const length = bufferLine.loadCell(i, this._workCell).getChars().length;
if (this._workCell.getWidth() === 0) {
// Wide characters aren't included in the line string so decrement the
// index so the index is back on the wide character.
charIndex--;
} else if (length > 1 && coords[0] !== i) {
} else if (length > 1 && x !== i) {
// Emojis take up multiple characters, so adjust accordingly. For these
// we don't want ot include the character at the column as we're
// returning the start index in the string, not the end index.
@@ -816,7 +817,7 @@ export class SelectionService extends Disposable implements ISelectionService {
const line = buffer.translateBufferLineToString(coords[1], false);
// Get actual index, taking into consideration wide characters
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords[0]);
let endIndex = startIndex;
// Record offset to be used later
@@ -1000,7 +1001,7 @@ export class SelectionService extends Disposable implements ISelectionService {
/**
* Gets whether the character is considered a word separator by the select
* word logic.
* @param char The character to check.
* @param cell The cell to check.
*/
private _isCharWordSeparator(cell: CellData): boolean {
// Zero width characters are never separators as they are always to the
+2 -2
View File
@@ -82,7 +82,7 @@ export class CircularList<T> extends Disposable implements ICircularList<T> {
* Note that for performance reasons there is no bounds checking here, the index reference is
* circular so this should always return a value and never throw.
* @param index The index of the value to get.
* @return The value corresponding to the index.
* @returns The value corresponding to the index.
*/
public get(index: number): T | undefined {
return this._array[this._getCyclicIndex(index)];
@@ -138,7 +138,7 @@ export class CircularList<T> extends Disposable implements ICircularList<T> {
/**
* Removes and returns the last value on the list.
* @return The popped value.
* @returns The popped value.
*/
public pop(): T | undefined {
return this._array[this._getCyclicIndex(this._length-- - 1)];
+1
View File
@@ -184,6 +184,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
/**
* Scroll the terminal down 1 row, creating a blank line.
* @param eraseAttr The attribute data to use the for blank line.
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
+33 -2
View File
@@ -8,8 +8,8 @@ import { InputHandler } from 'common/InputHandler';
import { IBufferLine, IAttributeData, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Attributes, BgFlags, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { IBufferService, ICoreService } from 'common/services/Services';
@@ -1689,6 +1689,37 @@ describe('InputHandler', () => {
});
});
describe('reset text attributes (SGR 0)', () => {
it('resets all attributes if there is no url', async () => {
await inputHandler.parseP('\x1b[30m\x1b[40m\x1b[4m');
assert.notEqual(inputHandler.curAttrData.fg, 0);
assert.notEqual(inputHandler.curAttrData.bg, 0);
assert.isFalse(inputHandler.curAttrData.extended.isEmpty());
await inputHandler.parseP('\x1b[m');
assert.equal(inputHandler.curAttrData.fg, 0);
assert.equal(inputHandler.curAttrData.bg, 0);
assert.isTrue(inputHandler.curAttrData.extended.isEmpty());
});
it('resets all attributes except for the url', async () => {
await inputHandler.parseP('\x1b[30m\x1b[40m\x1b[4m');
await inputHandler.parseP('\x1b]8;;http://example.com\x1b\\');
assert.notEqual(inputHandler.curAttrData.fg, 0);
assert.notEqual(inputHandler.curAttrData.bg, 0);
assert.notEqual(inputHandler.curAttrData.extended.ext, 0);
const urlId = inputHandler.curAttrData.extended.urlId;
assert.notEqual(urlId, 0);
await inputHandler.parseP('\x1b[m');
assert.equal(inputHandler.curAttrData.fg, 0);
assert.equal(inputHandler.curAttrData.bg, BgFlags.HAS_EXTENDED);
const expectedExtended = new ExtendedAttrs();
expectedExtended.urlId = urlId;
assert.deepEqual(inputHandler.curAttrData.extended, expectedExtended);
});
});
describe('extended underline style support (SGR 4)', () => {
beforeEach(() => {
bufferService.resize(10, 5);
+27 -16
View File
@@ -11,7 +11,7 @@ import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
import { Disposable } from 'common/Lifecycle';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { EventEmitter } from 'common/EventEmitter';
import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
@@ -121,7 +121,6 @@ export class InputHandler extends Disposable implements IInputHandler {
private _workCell: CellData = new CellData();
private _windowTitle = '';
private _iconName = '';
private _currentLinkId?: number;
private _dirtyRowTracker: IDirtyRowTracker;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
@@ -406,6 +405,10 @@ export class InputHandler extends Disposable implements IInputHandler {
}
}
private _getCurrentLinkId(): number {
return this._curAttrData.extended.urlId;
}
/**
* Parse call with async handler support.
*
@@ -533,8 +536,8 @@ export class InputHandler extends Disposable implements IInputHandler {
if (screenReaderMode) {
this._onA11yChar.fire(stringFromCodePoint(code));
}
if (this._currentLinkId !== undefined) {
this._oscLinkService.addLineToLink(this._currentLinkId, this._activeBuffer.ybase + this._activeBuffer.y);
if (this._getCurrentLinkId()) {
this._oscLinkService.addLineToLink(this._getCurrentLinkId(), this._activeBuffer.ybase + this._activeBuffer.y);
}
// insert combining char at last cursor position
@@ -1116,10 +1119,11 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* Helper method to erase cells in a terminal row.
* The cell gets replaced with the eraseChar of the terminal.
* @param y row index
* @param start first cell index to be erased
* @param end end - 1 is last erased cell
* @param cleanWrap clear the isWrapped flag
* @param y The row index relative to the viewport.
* @param start The start x index of the range to be erased.
* @param end The end x index of the range to be erased (exclusive).
* @param clearWrap clear the isWrapped flag
* @param respectProtect Whether to respect the protection attribute (DECSCA).
*/
private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
@@ -2352,6 +2356,17 @@ export class InputHandler extends Disposable implements IInputHandler {
attr.updateExtended();
}
private _processSGR0(attr: IAttributeData): void {
attr.fg = DEFAULT_ATTR_DATA.fg;
attr.bg = DEFAULT_ATTR_DATA.bg;
attr.extended = attr.extended.clone();
// Reset underline style and color. Note that we don't want to reset other
// fields such as the url id.
attr.extended.underlineStyle = UnderlineStyle.NONE;
attr.extended.underlineColor &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
attr.updateExtended();
}
/**
* CSI Pm m Character Attributes (SGR).
*
@@ -2437,8 +2452,7 @@ export class InputHandler extends Disposable implements IInputHandler {
public charAttributes(params: IParams): boolean {
// Optimize a single SGR0.
if (params.length === 1 && params.params[0] === 0) {
this._curAttrData.fg = DEFAULT_ATTR_DATA.fg;
this._curAttrData.bg = DEFAULT_ATTR_DATA.bg;
this._processSGR0(this._curAttrData);
return true;
}
@@ -2466,8 +2480,7 @@ export class InputHandler extends Disposable implements IInputHandler {
attr.bg |= Attributes.CM_P16 | (p - 100) | 8;
} else if (p === 0) {
// default
attr.fg = DEFAULT_ATTR_DATA.fg;
attr.bg = DEFAULT_ATTR_DATA.bg;
this._processSGR0(attr);
} else if (p === 1) {
// bold text
attr.fg |= FgFlags.BOLD;
@@ -2934,7 +2947,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _createHyperlink(params: string, uri: string): boolean {
// It's legal to open a new hyperlink without explicitly finishing the previous one
if (this._currentLinkId !== undefined) {
if (this._getCurrentLinkId()) {
this._finishHyperlink();
}
const parsedParams = params.split(':');
@@ -2944,8 +2957,7 @@ export class InputHandler extends Disposable implements IInputHandler {
id = parsedParams[idParamIndex].slice(3) || undefined;
}
this._curAttrData.extended = this._curAttrData.extended.clone();
this._currentLinkId = this._oscLinkService.registerLink({ id, uri });
this._curAttrData.extended.urlId = this._currentLinkId;
this._curAttrData.extended.urlId = this._oscLinkService.registerLink({ id, uri });
this._curAttrData.updateExtended();
return true;
}
@@ -2954,7 +2966,6 @@ export class InputHandler extends Disposable implements IInputHandler {
this._curAttrData.extended = this._curAttrData.extended.clone();
this._curAttrData.extended.urlId = 0;
this._curAttrData.updateExtended();
this._currentLinkId = undefined;
return true;
}
+3 -6
View File
@@ -17,18 +17,15 @@ export abstract class Disposable implements IDisposable {
}
/**
* Disposes the object, triggering the `dispose` method on all registered IDisposables. This is a
* readonly property instead of a method to prevent subclasses overriding it which is an easy
* mistake that can introduce memory leaks. If a class extends Disposable, all dispose calls
* should be done via {@link register}.
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
*/
public readonly dispose = (): void => {
public dispose(): void {
this._isDisposed = true;
for (const d of this._disposables) {
d.dispose();
}
this._disposables.length = 0;
};
}
/**
* Registers a disposable object.
+10
View File
@@ -20,6 +20,16 @@ const platform = (isNode) ? 'node' : navigator.platform;
export const isFirefox = userAgent.includes('Firefox');
export const isLegacyEdge = userAgent.includes('Edge');
export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
export function getSafariVersion(): number {
if (!isSafari) {
return 0;
}
const majorVersion = userAgent.match(/Version\/(\d+)/);
if (majorVersion === null || majorVersion.length < 2) {
return 0;
}
return parseInt(majorVersion[1]);
}
// Find the users platform. We use this to interpret the meta key
// and ISO third level shifts.
+1 -72
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert } from 'chai';
import { fillFallback, concat } from 'common/TypedArrayUtils';
import { concat } from 'common/TypedArrayUtils';
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array;
@@ -14,77 +14,6 @@ function deepEquals(a: TypedArray, b: TypedArray): void {
}
}
describe('polyfill conformance tests', function(): void {
describe('TypedArray.fill', function(): void {
it('should work with all typed array types', function(): void {
const u81 = new Uint8Array(5);
const u82 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2), u82.fill(2));
deepEquals(fillFallback(u81, -1), u82.fill(-1));
const u161 = new Uint16Array(5);
const u162 = new Uint16Array(5);
deepEquals(fillFallback(u161, 2), u162.fill(2));
deepEquals(fillFallback(u161, 65535), u162.fill(65535));
deepEquals(fillFallback(u161, -1), u162.fill(-1));
const u321 = new Uint32Array(5);
const u322 = new Uint32Array(5);
deepEquals(fillFallback(u321, 2), u322.fill(2));
deepEquals(fillFallback(u321, 65537), u322.fill(65537));
deepEquals(fillFallback(u321, -1), u322.fill(-1));
const i81 = new Int8Array(5);
const i82 = new Int8Array(5);
deepEquals(fillFallback(i81, 2), i82.fill(2));
deepEquals(fillFallback(i81, -1), i82.fill(-1));
const i161 = new Int16Array(5);
const i162 = new Int16Array(5);
deepEquals(fillFallback(i161, 2), i162.fill(2));
deepEquals(fillFallback(i161, 65535), i162.fill(65535));
deepEquals(fillFallback(i161, -1), i162.fill(-1));
const i321 = new Int32Array(5);
const i322 = new Int32Array(5);
deepEquals(fillFallback(i321, 2), i322.fill(2));
deepEquals(fillFallback(i321, 65537), i322.fill(65537));
deepEquals(fillFallback(i321, -1), i322.fill(-1));
const f321 = new Float32Array(5);
const f322 = new Float32Array(5);
deepEquals(fillFallback(f321, 1.2345), f322.fill(1.2345));
const f641 = new Float64Array(5);
const f642 = new Float64Array(5);
deepEquals(fillFallback(f641, 1.2345), f642.fill(1.2345));
const u8Clamped1 = new Uint8ClampedArray(5);
const u8Clamped2 = new Uint8ClampedArray(5);
deepEquals(fillFallback(u8Clamped1, 2), u8Clamped2.fill(2));
deepEquals(fillFallback(u8Clamped1, 257), u8Clamped2.fill(257));
});
it('start offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, i), u83.fill(2, i));
deepEquals(fillFallback(u81, -1, i), u83.fill(-1, i));
}
});
it('end offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, 0, i), u83.fill(2, 0, i));
deepEquals(fillFallback(u81, -1, 0, i), u83.fill(-1, 0, i));
}
});
it('start/end offset', function(): void {
for (let i = -2; i < 10; ++i) {
for (let j = -2; j < 10; ++j) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, i, j), u83.fill(2, i, j));
deepEquals(fillFallback(u81, -1, i, j), u83.fill(-1, i, j));
}
}
});
});
});
describe('typed array convenience functions', () => {
it('concat', () => {
const a = new Uint8Array([1, 2, 3, 4, 5]);
-33
View File
@@ -5,39 +5,6 @@
export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array;
/**
* polyfill for TypedArray.fill
* This is needed to support .fill in all safari versions and IE 11.
*/
export function fill<T extends TypedArray>(array: T, value: number, start?: number, end?: number): T {
// all modern engines that support .fill
if (array.fill) {
return array.fill(value, start, end) as T;
}
return fillFallback(array, value, start, end);
}
export function fillFallback<T extends TypedArray>(array: T, value: number, start: number = 0, end: number = array.length): T {
// safari and IE 11
// since IE 11 does not support Array.prototype.fill either
// we cannot use the suggested polyfill from MDN
// instead we simply fall back to looping
if (start >= array.length) {
return array;
}
start = (array.length + start) % array.length;
if (end >= array.length) {
end = array.length;
} else {
end = (array.length + end) % array.length;
}
for (let i = start; i < end; ++i) {
array[i] = value;
}
return array;
}
/**
* Concat two typed arrays `a` and `b`.
* Returns a new typed array.
+3 -4
View File
@@ -33,8 +33,7 @@ export class Buffer implements IBuffer {
public x: number = 0;
public scrollBottom: number;
public scrollTop: number;
// TODO: Type me
public tabs: any;
public tabs: { [column: number]: boolean | undefined } = {};
public savedY: number = 0;
public savedX: number = 0;
public savedCurAttrData = DEFAULT_ATTR_DATA.clone();
@@ -513,7 +512,7 @@ export class Buffer implements IBuffer {
* TODO: respect trim flag after fixing #1685
* @param lineIndex line index the string was retrieved from
* @param stringIndex index within the string
* @param startCol column offset the string was retrieved from
* @param trimRight Whether to trim whitespace to the right.
*/
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex {
while (stringIndex) {
@@ -542,7 +541,7 @@ export class Buffer implements IBuffer {
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param line The line being translated.
* @param lineIndex The absolute index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
* @param startCol The column to start at.
* @param endCol The column to end at.
+1 -1
View File
@@ -133,7 +133,7 @@ export class BufferLine implements IBufferLine {
* Test whether contains any chars.
* Basically an empty has no content, but other cells might differ in FG/BG
* from real empty cells.
* */
*/
public hasContent(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
}
+3
View File
@@ -16,7 +16,10 @@ export interface INewLayoutResult {
* Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
* when a wrapped line unwraps.
* @param lines The buffer lines.
* @param oldCols The columns before resize
* @param newCols The columns after resize.
* @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY).
* @param nullCell The cell data to use when filling in empty cells.
*/
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
-1
View File
@@ -24,7 +24,6 @@ export class BufferSet extends Disposable implements IBufferSet {
/**
* Create a new BufferSet for the given terminal.
* @param _terminal - The terminal the BufferSet will belong to
*/
constructor(
private readonly _optionsService: IOptionsService,
+1 -1
View File
@@ -47,7 +47,7 @@ export class CellData extends AttributeData implements ICellData {
* Note this returns the UTF32 codepoint of single chars,
* if content is a combined string it returns the codepoint
* of the last char in string to be in line with code in CharData.
* */
*/
public getCode(): number {
return (this.isCombined())
? this.combinedData.charCodeAt(this.combinedData.length - 1)
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
export const DEFAULT_COLOR = 256;
export const DEFAULT_COLOR = 0;
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
export const DEFAULT_EXT = 0;
+12 -13
View File
@@ -3,7 +3,6 @@
* @license MIT
*/
import { IUnicodeVersionProvider } from 'common/services/Services';
import { fill } from 'common/TypedArrayUtils';
type CharWidth = 0 | 1 | 2;
@@ -90,26 +89,26 @@ export class UnicodeV6 implements IUnicodeVersionProvider {
// init lookup table once
if (!table) {
table = new Uint8Array(65536);
fill(table, 1);
table.fill(1);
table[0] = 0;
// control chars
fill(table, 0, 1, 32);
fill(table, 0, 0x7f, 0xa0);
table.fill(0, 1, 32);
table.fill(0, 0x7f, 0xa0);
// apply wide char rules first
// wide chars
fill(table, 2, 0x1100, 0x1160);
table.fill(2, 0x1100, 0x1160);
table[0x2329] = 2;
table[0x232a] = 2;
fill(table, 2, 0x2e80, 0xa4d0);
table.fill(2, 0x2e80, 0xa4d0);
table[0x303f] = 1; // wrongly in last line
fill(table, 2, 0xac00, 0xd7a4);
fill(table, 2, 0xf900, 0xfb00);
fill(table, 2, 0xfe10, 0xfe1a);
fill(table, 2, 0xfe30, 0xfe70);
fill(table, 2, 0xff00, 0xff61);
fill(table, 2, 0xffe0, 0xffe7);
table.fill(2, 0xac00, 0xd7a4);
table.fill(2, 0xf900, 0xfb00);
table.fill(2, 0xfe10, 0xfe1a);
table.fill(2, 0xfe30, 0xfe70);
table.fill(2, 0xff00, 0xff61);
table.fill(2, 0xffe0, 0xffe7);
// apply combining last to ensure we overwrite
// wrongly wide set chars:
@@ -117,7 +116,7 @@ export class UnicodeV6 implements IUnicodeVersionProvider {
// through to wide check so we simply do here the opposite
// combining 0
for (let r = 0; r < BMP_COMBINING.length; ++r) {
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
}
}
}
+2 -2
View File
@@ -24,8 +24,8 @@ export const enum ParserState {
}
/**
* Internal actions of EscapeSequenceParser.
*/
* Internal actions of EscapeSequenceParser.
*/
export const enum ParserAction {
IGNORE = 0,
ERROR = 1,
+1 -2
View File
@@ -7,7 +7,6 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler
import { ParserState, ParserAction } from 'common/parser/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { fill } from 'common/TypedArrayUtils';
import { Params } from 'common/parser/Params';
import { OscParser } from 'common/parser/OscParser';
import { DcsParser } from 'common/parser/DcsParser';
@@ -39,7 +38,7 @@ export class TransitionTable {
* @param next default next state
*/
public setDefault(action: ParserAction, next: ParserState): void {
fill(this.table, action << TableAccess.TRANSITION_ACTION_SHIFT | next);
this.table.fill(action << TableAccess.TRANSITION_ACTION_SHIFT | next);
}
/**
+2 -2
View File
@@ -142,8 +142,8 @@ export type PrintFallbackHandlerType = PrintHandlerType;
/**
* EscapeSequenceParser interface.
*/
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser extends IDisposable {
/**
* Preceding codepoint to get REP working correctly.
+1
View File
@@ -54,6 +54,7 @@ export class BufferService extends Disposable implements IBufferService {
/**
* Scroll the terminal down 1 row, creating a blank line.
* @param eraseAttr The attribute data to use the for blank line.
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
+13 -8
View File
@@ -42,6 +42,7 @@ export class DecorationService extends Disposable implements IDecorationService
this.reset();
}));
}
public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
if (options.marker.isDisposed) {
return undefined;
@@ -91,19 +92,25 @@ export class DecorationService extends Disposable implements IDecorationService
}
});
}
public dispose(): void {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
}
this.reset();
}
}
class Decoration extends Disposable implements IInternalDecoration {
public readonly marker: IMarker;
public element: HTMLElement | undefined;
public isDisposed: boolean = false;
public readonly onRenderEmitter = this.register(new EventEmitter<HTMLElement>());
public readonly onRender = this.onRenderEmitter.event;
private readonly _onDispose = this.register(new EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
public get isDisposed(): boolean { return this._isDisposed; }
private _cachedBg: IColor | undefined | null = null;
public get backgroundColorRGB(): IColor | undefined {
if (this._cachedBg === null) {
@@ -136,12 +143,10 @@ class Decoration extends Disposable implements IInternalDecoration {
if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) {
this.options.overviewRulerOptions.position = 'full';
}
}
this.register(toDisposable(() => {
if (this._isDisposed) {
return;
}
this._onDispose.fire();
}));
public override dispose(): void {
this._onDispose.fire();
super.dispose();
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ describe('Headless API Tests', function (): void {
it('Proposed API check', async () => {
term = new Terminal({ allowProposedApi: false });
throws(() => term.buffer, (error) => error.message === 'You must set the allowProposedApi option to true to use proposed API');
throws(() => term.markers, (error) => error.message === 'You must set the allowProposedApi option to true to use proposed API');
});
it('write', async () => {