Merge branch 'master' into buffer_optimizations
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ export const enum ParserState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal actions of EscapeSequenceParser.
|
||||
*/
|
||||
* Internal actions of EscapeSequenceParser.
|
||||
*/
|
||||
export const enum ParserAction {
|
||||
IGNORE = 0,
|
||||
ERROR = 1,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+2
-2
@@ -142,8 +142,8 @@ export type PrintFallbackHandlerType = PrintHandlerType;
|
||||
|
||||
|
||||
/**
|
||||
* EscapeSequenceParser interface.
|
||||
*/
|
||||
* EscapeSequenceParser interface.
|
||||
*/
|
||||
export interface IEscapeSequenceParser extends IDisposable {
|
||||
/**
|
||||
* Preceding codepoint to get REP working correctly.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user