Merge remote-tracking branch 'upstream/master' into renderer_before_open
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types';
|
||||
import { ITheme } from 'common/services/Services';
|
||||
import { channels, color, css } from 'common/Color';
|
||||
import { channels, color, css, NULL_COLOR } from 'common/Color';
|
||||
import { ColorContrastCache } from 'browser/ColorContrastCache';
|
||||
import { ColorIndex, IColor } from 'common/Types';
|
||||
|
||||
@@ -124,12 +124,8 @@ export class ColorManager implements IColorManager {
|
||||
this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent);
|
||||
this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent);
|
||||
this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent);
|
||||
const nullColor: IColor = {
|
||||
css: '',
|
||||
rgba: 0
|
||||
};
|
||||
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
|
||||
if (this.colors.selectionForeground === nullColor) {
|
||||
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, NULL_COLOR) : undefined;
|
||||
if (this.colors.selectionForeground === NULL_COLOR) {
|
||||
this.colors.selectionForeground = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined;
|
||||
private _activeLine: number = -1;
|
||||
|
||||
private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
|
||||
private _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; }
|
||||
private readonly _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public readonly onShowLinkUnderline = this._onShowLinkUnderline.event;
|
||||
private readonly _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
|
||||
+13
-14
@@ -22,7 +22,7 @@
|
||||
*/
|
||||
|
||||
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
|
||||
import { IRenderer } from 'browser/renderer/Types';
|
||||
import { IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||
import { Viewport } from 'browser/Viewport';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
|
||||
@@ -122,18 +122,18 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _colorManager: ColorManager | undefined;
|
||||
private _theme: ITheme | undefined;
|
||||
|
||||
private _onCursorMove = new EventEmitter<void>();
|
||||
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
|
||||
private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
|
||||
private _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
private _onSelectionChange = new EventEmitter<void>();
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
private _onTitleChange = new EventEmitter<string>();
|
||||
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
|
||||
private _onBell = new EventEmitter<void>();
|
||||
public get onBell(): IEvent<void> { return this._onBell.event; }
|
||||
private readonly _onCursorMove = new EventEmitter<void>();
|
||||
public readonly onCursorMove = this._onCursorMove.event;
|
||||
private readonly _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
|
||||
public readonly onKey = this._onKey.event;
|
||||
private readonly _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public readonly onRender = this._onRender.event;
|
||||
private readonly _onSelectionChange = new EventEmitter<void>();
|
||||
public readonly onSelectionChange = this._onSelectionChange.event;
|
||||
private readonly _onTitleChange = new EventEmitter<string>();
|
||||
public readonly onTitleChange = this._onTitleChange.event;
|
||||
private readonly _onBell = new EventEmitter<void>();
|
||||
public readonly onBell = this._onBell.event;
|
||||
|
||||
private _onFocus = new EventEmitter<void>();
|
||||
public get onFocus(): IEvent<void> { return this._onFocus.event; }
|
||||
@@ -146,7 +146,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _onWillOpen = new EventEmitter<HTMLElement>();
|
||||
public get onWillOpen(): IEvent<HTMLElement> { return this._onWillOpen.event; }
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new `Terminal` object.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IColorSet, IViewport } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
|
||||
Vendored
-56
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface IRequestRedrawEvent {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that IRenderer implementations should emit the refresh event after
|
||||
* rendering rows to the screen.
|
||||
*/
|
||||
export interface IRenderer extends IDisposable {
|
||||
readonly dimensions: IRenderDimensions;
|
||||
|
||||
/**
|
||||
* Fires when the renderer is requesting to be redrawn on the next animation
|
||||
* frame but is _not_ a result of content changing (eg. selection changes).
|
||||
*/
|
||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||
|
||||
dispose(): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
renderRows(start: number, end: number): void;
|
||||
clearTextureAtlas?(): void;
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
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/Constants';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
|
||||
@@ -40,7 +40,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return new EventEmitter<IRequestRedrawEvent>().event; }
|
||||
public readonly onRequestRedraw = new EventEmitter<IRequestRedrawEvent>().event;
|
||||
|
||||
constructor(
|
||||
private _colors: IColorSet,
|
||||
@@ -231,11 +231,11 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
` background-color: ${this._colors.selectionInactiveBackgroundOpaque.css};` +
|
||||
`}`;
|
||||
// Colors
|
||||
this._colors.ansi.forEach((c, i) => {
|
||||
for (const [i, c] of this._colors.ansi.entries()) {
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
|
||||
});
|
||||
}
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IBufferLine, ICellData, IColor } from 'common/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
@@ -12,7 +12,7 @@ import { color, rgba } from 'common/Color';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
@@ -166,7 +166,7 @@ export class DomRendererRowFactory {
|
||||
if (cell.isUnderline()) {
|
||||
charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`);
|
||||
if (charElement.textContent === ' ') {
|
||||
charElement.innerHTML = ' ';
|
||||
charElement.textContent = '\xa0'; // =
|
||||
}
|
||||
if (!cell.isUnderlineColorDefault()) {
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $fg = 0;
|
||||
let $bg = 0;
|
||||
let $hasFg = false;
|
||||
let $hasBg = false;
|
||||
let $isSelected = false;
|
||||
|
||||
export class CellColorResolver {
|
||||
/**
|
||||
* The shared result of the {@link resolve} call. This is only safe to use immediately after as
|
||||
* any other calls will share object.
|
||||
*/
|
||||
public readonly result: { fg: number, bg: number, ext: number } = {
|
||||
fg: 0,
|
||||
bg: 0,
|
||||
ext: 0
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private readonly _selectionRenderModel: ISelectionRenderModel,
|
||||
private readonly _decorationService: IDecorationService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
|
||||
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
|
||||
*/
|
||||
public resolve(cell: ICellData, x: number, y: number): void {
|
||||
this.result.bg = cell.bg;
|
||||
this.result.fg = cell.fg;
|
||||
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
|
||||
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
||||
// override logic throughout the different sub-renderers
|
||||
|
||||
// Reset overrides work variables
|
||||
$bg = 0;
|
||||
$fg = 0;
|
||||
$hasBg = false;
|
||||
$hasFg = false;
|
||||
$isSelected = false;
|
||||
|
||||
// Apply decorations on the bottom layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasFg = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the selection color if needed
|
||||
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
|
||||
if ($isSelected) {
|
||||
$bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
|
||||
$hasBg = true;
|
||||
if (this._colors.selectionForeground) {
|
||||
$fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
$hasFg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasFg = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
|
||||
// ahead of time in order to use the correct cache key
|
||||
if ($hasBg) {
|
||||
if ($isSelected) {
|
||||
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
|
||||
$bg = (cell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB;
|
||||
} else {
|
||||
// Non-RGB attributes from model + override + force RGB color mode
|
||||
$bg = (cell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB;
|
||||
}
|
||||
}
|
||||
if ($hasFg) {
|
||||
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
||||
$fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB;
|
||||
}
|
||||
|
||||
// Handle case where inverse was specified by only one of bg override or fg override was set,
|
||||
// resolving the other inverse color and setting the inverse flag if needed.
|
||||
if (this.result.fg & FgFlags.INVERSE) {
|
||||
if ($hasBg && !$hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
$hasFg = true;
|
||||
}
|
||||
if (!$hasBg && $hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
$hasBg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the override if it exists
|
||||
this.result.bg = $hasBg ? $bg : this.result.bg;
|
||||
this.result.fg = $hasFg ? $fg : this.result.fg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet, ITerminal } from 'browser/Types';
|
||||
import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils';
|
||||
|
||||
interface ITextureAtlasCacheEntry {
|
||||
atlas: ITextureAtlas;
|
||||
config: ICharAtlasConfig;
|
||||
// N.B. This implementation potentially holds onto copies of the terminal forever, so
|
||||
// this may cause memory leaks.
|
||||
ownedBy: Terminal[];
|
||||
}
|
||||
|
||||
const charAtlasCache: ITextureAtlasCacheEntry[] = [];
|
||||
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
* @param terminal The terminal.
|
||||
* @param colors The colors to use.
|
||||
*/
|
||||
export function acquireTextureAtlas(
|
||||
terminal: Terminal,
|
||||
colors: IColorSet,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number,
|
||||
scaledCharWidth: number,
|
||||
scaledCharHeight: number,
|
||||
devicePixelRatio: number
|
||||
): ITextureAtlas {
|
||||
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
const ownedByIndex = entry.ownedBy.indexOf(terminal);
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.atlas;
|
||||
}
|
||||
// The configs differ, release the terminal from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
entry.atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try match a char atlas from the cache
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
// Add the terminal to the cache entry and return
|
||||
entry.ownedBy.push(terminal);
|
||||
return entry.atlas;
|
||||
}
|
||||
}
|
||||
|
||||
const core: ITerminal = (terminal as any)._core;
|
||||
const newEntry: ITextureAtlasCacheEntry = {
|
||||
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.atlas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a terminal reference from the cache, allowing its memory to be freed.
|
||||
* @param terminal The terminal to remove.
|
||||
*/
|
||||
export function removeTerminalFromCache(terminal: Terminal): void {
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
|
||||
if (index !== -1) {
|
||||
if (charAtlasCache[i].ownedBy.length === 1) {
|
||||
// Remove the cache entry if it's the only terminal
|
||||
charAtlasCache[i].atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
// Remove the reference from the cache entry
|
||||
charAtlasCache[i].ownedBy.splice(index, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { Attributes } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { NULL_COLOR } from 'common/Color';
|
||||
|
||||
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors: IColorSet = {
|
||||
foreground: colors.foreground,
|
||||
background: colors.background,
|
||||
cursor: NULL_COLOR,
|
||||
cursorAccent: NULL_COLOR,
|
||||
selectionForeground: NULL_COLOR,
|
||||
selectionBackgroundTransparent: NULL_COLOR,
|
||||
selectionBackgroundOpaque: NULL_COLOR,
|
||||
selectionInactiveBackgroundTransparent: NULL_COLOR,
|
||||
selectionInactiveBackgroundOpaque: NULL_COLOR,
|
||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||
// dynamic character atlas.
|
||||
ansi: colors.ansi.slice(),
|
||||
contrastCache: colors.contrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: terminal.options.customGlyphs,
|
||||
devicePixelRatio,
|
||||
letterSpacing: terminal.options.letterSpacing,
|
||||
lineHeight: terminal.options.lineHeight,
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.options.fontFamily,
|
||||
fontSize: terminal.options.fontSize,
|
||||
fontWeight: terminal.options.fontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold,
|
||||
allowTransparency: terminal.options.allowTransparency,
|
||||
drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors,
|
||||
minimumContrastRatio: terminal.options.minimumContrastRatio,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
for (let i = 0; i < a.colors.ansi.length; i++) {
|
||||
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a.devicePixelRatio === b.devicePixelRatio &&
|
||||
a.customGlyphs === b.customGlyphs &&
|
||||
a.lineHeight === b.lineHeight &&
|
||||
a.letterSpacing === b.letterSpacing &&
|
||||
a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
|
||||
a.minimumContrastRatio === b.minimumContrastRatio &&
|
||||
a.colors.foreground.rgba === b.colors.foreground.rgba &&
|
||||
a.colors.background.rgba === b.colors.background.rgba;
|
||||
}
|
||||
|
||||
export function is256Color(colorCode: number): boolean {
|
||||
return (colorCode & Attributes.CM_MASK) === Attributes.CM_P16 || (colorCode & Attributes.CM_MASK) === Attributes.CM_P256;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
interface IBlockVector {
|
||||
x: number;
|
||||
@@ -0,0 +1 @@
|
||||
This folder contains files that are shared between the renderer addons, but not necessarily bundled into the `xterm` module.
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
class SelectionRenderModel implements ISelectionRenderModel {
|
||||
public hasSelection!: boolean;
|
||||
public columnSelectMode!: boolean;
|
||||
public viewportStartRow!: number;
|
||||
public viewportEndRow!: number;
|
||||
public viewportCappedStartRow!: number;
|
||||
public viewportCappedEndRow!: number;
|
||||
public startCol!: number;
|
||||
public endCol!: number;
|
||||
public selectionStart: [number, number] | undefined;
|
||||
public selectionEnd: [number, number] | undefined;
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.hasSelection = false;
|
||||
this.columnSelectMode = false;
|
||||
this.viewportStartRow = 0;
|
||||
this.viewportEndRow = 0;
|
||||
this.viewportCappedStartRow = 0;
|
||||
this.viewportCappedEndRow = 0;
|
||||
this.startCol = 0;
|
||||
this.endCol = 0;
|
||||
this.selectionStart = undefined;
|
||||
this.selectionEnd = undefined;
|
||||
}
|
||||
|
||||
public update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this.selectionStart = start;
|
||||
this.selectionEnd = end;
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
|
||||
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasSelection = true;
|
||||
this.columnSelectMode = columnSelectMode;
|
||||
this.viewportStartRow = viewportStartRow;
|
||||
this.viewportEndRow = viewportEndRow;
|
||||
this.viewportCappedStartRow = viewportCappedStartRow;
|
||||
this.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this.startCol = start[0];
|
||||
this.endCol = end[0];
|
||||
}
|
||||
|
||||
public isCellSelected(terminal: Terminal, x: number, y: number): boolean {
|
||||
if (!this.hasSelection) {
|
||||
return false;
|
||||
}
|
||||
y -= terminal.buffer.active.viewportY;
|
||||
if (this.columnSelectMode) {
|
||||
if (this.startCol <= this.endCol) {
|
||||
return x >= this.startCol && y >= this.viewportCappedStartRow &&
|
||||
x < this.endCol && y <= this.viewportCappedEndRow;
|
||||
}
|
||||
return x < this.startCol && y >= this.viewportCappedStartRow &&
|
||||
x >= this.endCol && y <= this.viewportCappedEndRow;
|
||||
}
|
||||
return (y > this.viewportStartRow && y < this.viewportEndRow) ||
|
||||
(this.viewportStartRow === this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol && x < this.endCol) ||
|
||||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportEndRow && x < this.endCol) ||
|
||||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol);
|
||||
}
|
||||
}
|
||||
|
||||
export function createSelectionRenderModel(): ISelectionRenderModel {
|
||||
return new SelectionRenderModel();
|
||||
}
|
||||
@@ -0,0 +1,842 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
|
||||
import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { IColor } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { color, NULL_COLOR, rgba } from 'common/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
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';
|
||||
|
||||
// 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 },
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: { x: 0, y: 0 },
|
||||
sizeClipSpace: { x: 0, y: 0 }
|
||||
};
|
||||
|
||||
const TMP_CANVAS_GLYPH_PADDING = 2;
|
||||
|
||||
interface ICharAtlasActiveRow {
|
||||
x: number;
|
||||
y: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $glyph = undefined;
|
||||
|
||||
export class TextureAtlas implements ITextureAtlas {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
private _cacheMap: FourKeyMap<number, number, number, number, IRasterizedGlyph> = new FourKeyMap();
|
||||
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 _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();
|
||||
|
||||
constructor(
|
||||
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._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {
|
||||
alpha: this._config.allowTransparency,
|
||||
willReadFrequently: true
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.cacheCanvas.parentElement) {
|
||||
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
public warmUp(): void {
|
||||
if (!this._didWarmUp) {
|
||||
this._doWarmUp();
|
||||
this._didWarmUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _doWarmUp(): void {
|
||||
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
|
||||
const queue = new IdleTaskQueue();
|
||||
for (let i = 33; i < 126; i++) {
|
||||
queue.enqueue(() => {
|
||||
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
|
||||
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
if (this._currentRow.y > TEXTURE_CAPACITY) {
|
||||
this.clearTexture();
|
||||
this.warmUp();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public clearTexture(): void {
|
||||
if (this._currentRow.x === 0 && this._currentRow.y === 0) {
|
||||
return;
|
||||
}
|
||||
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
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;
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext);
|
||||
}
|
||||
|
||||
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the glyphs texture coords, drawing the texture if it's not already
|
||||
*/
|
||||
private _getFromCacheMap(
|
||||
cacheMap: FourKeyMap<string | number, number, number, number, IRasterizedGlyph>,
|
||||
key: string | number,
|
||||
bg: number,
|
||||
fg: number,
|
||||
ext: number
|
||||
): IRasterizedGlyph {
|
||||
$glyph = cacheMap.get(key, bg, fg, ext);
|
||||
if (!$glyph) {
|
||||
$glyph = this._drawToCache(key, bg, fg, ext);
|
||||
cacheMap.set(key, bg, fg, ext, $glyph);
|
||||
}
|
||||
return $glyph;
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
if (idx >= this._config.colors.ansi.length) {
|
||||
throw new Error('No color found for idx ' + idx);
|
||||
}
|
||||
return this._config.colors.ansi[idx];
|
||||
}
|
||||
|
||||
private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean, dim: boolean): IColor {
|
||||
if (this._config.allowTransparency) {
|
||||
// The background color might have some transparency, so we need to render it as fully
|
||||
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
|
||||
// around the anti-aliased edges of the glyph, and it would look too dark.
|
||||
return NULL_COLOR;
|
||||
}
|
||||
|
||||
let result: IColor;
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
result = this._getColorFromAnsiIndex(bgColor);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(bgColor);
|
||||
// TODO: This object creation is slow
|
||||
result = rgba.toColor(arr[0], arr[1], arr[2]);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
result = this._config.colors.foreground;
|
||||
} else {
|
||||
result = this._config.colors.background;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (dim) {
|
||||
// Blend here instead of using opacity because transparent colors mess with clipping the
|
||||
// glyph's bounding box
|
||||
result = color.blend(this._config.colors.background, color.multiplyOpacity(result, DIM_OPACITY));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _getForegroundColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, dim: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor {
|
||||
// TODO: Pass dim along to get min contrast?
|
||||
const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, false, bold, excludeFromContrastRatioDemands);
|
||||
if (minimumContrastColor) {
|
||||
return minimumContrastColor;
|
||||
}
|
||||
|
||||
let result: IColor;
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
fgColor += 8;
|
||||
}
|
||||
result = this._getColorFromAnsiIndex(fgColor);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(fgColor);
|
||||
result = rgba.toColor(arr[0], arr[1], arr[2]);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
result = this._config.colors.background;
|
||||
} else {
|
||||
result = this._config.colors.foreground;
|
||||
}
|
||||
}
|
||||
|
||||
// Always use an opaque color regardless of allowTransparency
|
||||
if (this._config.allowTransparency) {
|
||||
result = color.opaque(result);
|
||||
}
|
||||
|
||||
// Apply dim to the color, opacity is fine to use for the foreground color
|
||||
if (dim) {
|
||||
result = color.multiplyOpacity(result, DIM_OPACITY);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
return this._getColorFromAnsiIndex(bgColor).rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return bgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._config.colors.foreground.rgba;
|
||||
}
|
||||
return this._config.colors.background.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
fgColor += 8;
|
||||
}
|
||||
return this._getColorFromAnsiIndex(fgColor).rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return fgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._config.colors.background.rgba;
|
||||
}
|
||||
return this._config.colors.foreground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _getMinimumContrastColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor | undefined {
|
||||
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try get from cache first
|
||||
const adjustedColor = this._config.colors.contrastCache.getColor(bg, fg);
|
||||
if (adjustedColor !== undefined) {
|
||||
return adjustedColor || undefined;
|
||||
}
|
||||
|
||||
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
|
||||
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
|
||||
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);
|
||||
|
||||
if (!result) {
|
||||
this._config.colors.contrastCache.setColor(bg, fg, null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const color = rgba.toColor(
|
||||
(result >> 24) & 0xFF,
|
||||
(result >> 16) & 0xFF,
|
||||
(result >> 8) & 0xFF
|
||||
);
|
||||
this._config.colors.contrastCache.setColor(bg, fg, color);
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
if (this._tmpCanvas.height < allowedHeight) {
|
||||
this._tmpCanvas.height = allowedHeight;
|
||||
}
|
||||
this._tmpCtx.save();
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
this._workAttributeData.bg = bg;
|
||||
this._workAttributeData.extended.ext = ext;
|
||||
|
||||
const invisible = !!this._workAttributeData.isInvisible();
|
||||
if (invisible) {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const bold = !!this._workAttributeData.isBold();
|
||||
const inverse = !!this._workAttributeData.isInverse();
|
||||
const dim = !!this._workAttributeData.isDim();
|
||||
const italic = !!this._workAttributeData.isItalic();
|
||||
const underline = !!this._workAttributeData.isUnderline();
|
||||
const strikethrough = !!this._workAttributeData.isStrikethrough();
|
||||
let fgColor = this._workAttributeData.getFgColor();
|
||||
let fgColorMode = this._workAttributeData.getFgColorMode();
|
||||
let bgColor = this._workAttributeData.getBgColor();
|
||||
let bgColorMode = this._workAttributeData.getBgColorMode();
|
||||
if (inverse) {
|
||||
const temp = fgColor;
|
||||
fgColor = bgColor;
|
||||
bgColor = temp;
|
||||
const temp2 = fgColorMode;
|
||||
fgColorMode = bgColorMode;
|
||||
bgColorMode = temp2;
|
||||
}
|
||||
|
||||
// draw the background
|
||||
const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse, dim);
|
||||
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
|
||||
// transparency in backgroundColor
|
||||
this._tmpCtx.globalCompositeOperation = 'copy';
|
||||
this._tmpCtx.fillStyle = backgroundColor.css;
|
||||
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
|
||||
this._tmpCtx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// draw the foreground/glyph
|
||||
const fontWeight = bold ? this._config.fontWeightBold : this._config.fontWeight;
|
||||
const fontStyle = italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
|
||||
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
|
||||
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
|
||||
this._tmpCtx.fillStyle = foregroundColor.css;
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
const padding = restrictedPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
||||
// background color. This should be disabled when the glyph contains multiple colors such as
|
||||
// underline colors to prevent important colors could get cleared.
|
||||
let enableClearThresholdCheck = !powerlineGlyph;
|
||||
|
||||
let chWidth: number;
|
||||
if (typeof codeOrChars === 'number') {
|
||||
chWidth = this._unicodeService.wcwidth(codeOrChars);
|
||||
} else {
|
||||
chWidth = this._unicodeService.getStringCellWidth(codeOrChars);
|
||||
}
|
||||
|
||||
// Draw underline
|
||||
if (underline) {
|
||||
this._tmpCtx.save();
|
||||
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 15));
|
||||
// When the line width is odd, draw at a 0.5 position
|
||||
const yOffset = lineWidth % 2 === 1 ? 0.5 : 0;
|
||||
this._tmpCtx.lineWidth = lineWidth;
|
||||
|
||||
// Underline color
|
||||
if (this._workAttributeData.isUnderlineColorDefault()) {
|
||||
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
|
||||
} else if (this._workAttributeData.isUnderlineColorRGB()) {
|
||||
enableClearThresholdCheck = false;
|
||||
this._tmpCtx.strokeStyle = `rgb(${AttributeData.toColorRGB(this._workAttributeData.getUnderlineColor()).join(',')})`;
|
||||
} else {
|
||||
enableClearThresholdCheck = false;
|
||||
let fg = this._workAttributeData.getUnderlineColor();
|
||||
if (this._config.drawBoldTextInBrightColors && this._workAttributeData.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
switch (this._workAttributeData.extended.underlineStyle) {
|
||||
case UnderlineStyle.DOUBLE:
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
this._tmpCtx.moveTo(xChLeft, yBot);
|
||||
this._tmpCtx.lineTo(xChRight, yBot);
|
||||
break;
|
||||
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;
|
||||
// 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);
|
||||
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.bezierCurveTo(
|
||||
xChLeft - this._config.scaledCellWidth / 2, yCurlyTop,
|
||||
xChLeft, yCurlyTop,
|
||||
xChLeft, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChLeft, yCurlyBot,
|
||||
xChMid, yCurlyBot,
|
||||
xChMid, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChMid, yCurlyTop,
|
||||
xChRight, yCurlyTop,
|
||||
xChRight, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChRight, yCurlyBot,
|
||||
xChRight + this._config.scaledCellWidth / 2, yCurlyBot,
|
||||
xChRight + this._config.scaledCellWidth / 2, yMid
|
||||
);
|
||||
break;
|
||||
case UnderlineStyle.DOTTED:
|
||||
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 2, this._config.devicePixelRatio]);
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
break;
|
||||
case UnderlineStyle.DASHED:
|
||||
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]);
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
break;
|
||||
case UnderlineStyle.SINGLE:
|
||||
default:
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
break;
|
||||
}
|
||||
this._tmpCtx.stroke();
|
||||
this._tmpCtx.restore();
|
||||
}
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// Draw stroke in the background color for non custom characters in order to give an outline
|
||||
// between the text and the underline. Only do this when font size is >= 12 as the underline
|
||||
// looks odd when the font size is too small
|
||||
if (!customGlyph && this._config.fontSize >= 12) {
|
||||
// This only works when transparency is disabled because it's not clear how to clear stroked
|
||||
// text
|
||||
if (!this._config.allowTransparency && chars !== ' ') {
|
||||
// Measure the text, only draw the stroke if there is a descent beyond an alphabetic text
|
||||
// baseline
|
||||
this._tmpCtx.save();
|
||||
this._tmpCtx.textBaseline = 'alphabetic';
|
||||
const metrics = this._tmpCtx.measureText(chars);
|
||||
this._tmpCtx.restore();
|
||||
if ('actualBoundingBoxDescent' in metrics && metrics.actualBoundingBoxDescent > 0) {
|
||||
// This translates to 1/2 the line width in either direction
|
||||
this._tmpCtx.save();
|
||||
// Clip the region to only draw in valid pixels near the underline to avoid a slight
|
||||
// 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));
|
||||
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.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
if (!customGlyph) {
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
}
|
||||
|
||||
// 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);
|
||||
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);
|
||||
if (!isBeyondCellBounds) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw strokethrough
|
||||
if (strikethrough) {
|
||||
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 10));
|
||||
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
|
||||
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.stroke();
|
||||
}
|
||||
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
// character if it extends past it's bounds
|
||||
const imageData = this._tmpCtx.getImageData(
|
||||
0, 0, this._tmpCanvas.width, this._tmpCanvas.height
|
||||
);
|
||||
|
||||
// Clear out the background color and determine if the glyph is empty.
|
||||
let isEmpty: boolean;
|
||||
if (!this._config.allowTransparency) {
|
||||
isEmpty = clearColor(imageData, backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
} else {
|
||||
isEmpty = checkCompletelyTransparent(imageData);
|
||||
}
|
||||
|
||||
// Handle empty glyphs
|
||||
if (isEmpty) {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
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 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) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If there is enough room in the current row, finish it and try again
|
||||
if (activeRow === this._currentRow) {
|
||||
activeRow.x = 0;
|
||||
activeRow.y += activeRow.height;
|
||||
activeRow.height = 0;
|
||||
} else {
|
||||
this._fixedRows.splice(this._fixedRows.indexOf(activeRow), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Record texture position
|
||||
rasterizedGlyph.texturePosition.x = activeRow.x;
|
||||
rasterizedGlyph.texturePosition.y = activeRow.y;
|
||||
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH;
|
||||
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / TEXTURE_HEIGHT;
|
||||
|
||||
// Update atlas current row, for fixed rows the glyph height will never be larger than the row
|
||||
// height
|
||||
activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y);
|
||||
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);
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ImageData object, find the bounding box of the non-transparent
|
||||
* portion of the texture and return an IRasterizedGlyph with these
|
||||
* dimensions.
|
||||
* @param imageData The image data to read.
|
||||
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
|
||||
*/
|
||||
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;
|
||||
let found = false;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.top = y;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.left = 0;
|
||||
found = false;
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.left = x;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.right = width;
|
||||
found = false;
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.right = x;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.bottom = height;
|
||||
found = false;
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.bottom = y;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: {
|
||||
x: boundingBox.right - boundingBox.left + 1,
|
||||
y: boundingBox.bottom - boundingBox.top + 1
|
||||
},
|
||||
sizeClipSpace: {
|
||||
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
|
||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||
},
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
return new ImageData(clippedData, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
|
||||
* transparent.
|
||||
* @returns True if the result is "empty", meaning all pixels are fully transparent.
|
||||
*/
|
||||
function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThresholdCheck: boolean): boolean {
|
||||
// Get color channels
|
||||
const r = bg.rgba >>> 24;
|
||||
const g = bg.rgba >>> 16 & 0xFF;
|
||||
const b = bg.rgba >>> 8 & 0xFF;
|
||||
const fgR = fg.rgba >>> 24;
|
||||
const fgG = fg.rgba >>> 16 & 0xFF;
|
||||
const fgB = fg.rgba >>> 8 & 0xFF;
|
||||
|
||||
// Calculate a threshold that when below a color will be treated as transpart when the sum of
|
||||
// channel value differs. This helps improve rendering when glyphs overlap with others. This
|
||||
// threshold is calculated relative to the difference between the background and foreground to
|
||||
// ensure important details of the glyph are always shown, even when the contrast ratio is low.
|
||||
// The number 12 is largely arbitrary to ensure the pixels that escape the cell in the test case
|
||||
// were covered (fg=#8ae234, bg=#c4a000).
|
||||
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);
|
||||
|
||||
// Set alpha channel of relevent pixels to 0
|
||||
let isEmpty = true;
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
// Check exact match
|
||||
if (imageData.data[offset] === r &&
|
||||
imageData.data[offset + 1] === g &&
|
||||
imageData.data[offset + 2] === b) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
} else {
|
||||
// Check the threshold based difference
|
||||
if (enableThresholdCheck &&
|
||||
(Math.abs(imageData.data[offset] - r) +
|
||||
Math.abs(imageData.data[offset + 1] - g) +
|
||||
Math.abs(imageData.data[offset + 2] - b)) < threshold) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isEmpty;
|
||||
}
|
||||
|
||||
function checkCompletelyTransparent(imageData: ImageData): boolean {
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
if (imageData.data[offset + 3] > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight, Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
customGlyphs: boolean;
|
||||
devicePixelRatio: number;
|
||||
letterSpacing: number;
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
drawBoldTextInBrightColors: boolean;
|
||||
minimumContrastRatio: number;
|
||||
colors: IColorSet;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface IRequestRedrawEvent {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that IRenderer implementations should emit the refresh event after
|
||||
* rendering rows to the screen.
|
||||
*/
|
||||
export interface IRenderer extends IDisposable {
|
||||
readonly dimensions: IRenderDimensions;
|
||||
|
||||
/**
|
||||
* Fires when the renderer is requesting to be redrawn on the next animation
|
||||
* frame but is _not_ a result of content changing (eg. selection changes).
|
||||
*/
|
||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||
|
||||
dispose(): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
renderRows(start: number, end: number): void;
|
||||
clearTextureAtlas?(): void;
|
||||
}
|
||||
|
||||
export interface ITextureAtlas extends IDisposable {
|
||||
readonly cacheCanvas: HTMLCanvasElement;
|
||||
hasCanvasChanged: boolean;
|
||||
|
||||
/**
|
||||
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
|
||||
*/
|
||||
warmUp(): void;
|
||||
|
||||
/**
|
||||
* Call when a frame is being drawn, this will return true if the atlas was cleared to make room
|
||||
* for a new set of glyphs.
|
||||
*/
|
||||
beginFrame(): boolean;
|
||||
|
||||
/**
|
||||
* Clear all glyphs from the texture atlas.
|
||||
*/
|
||||
clearTexture(): void;
|
||||
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph;
|
||||
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a rasterized glyph within a texture atlas. Some numbers are
|
||||
* tracked in CSS pixels as well in order to reduce calculations during the
|
||||
* render loop.
|
||||
*/
|
||||
export interface IRasterizedGlyph {
|
||||
/**
|
||||
* The x and y offset between the glyph's top/left and the top/left of a cell
|
||||
* in pixels.
|
||||
*/
|
||||
offset: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in pixels.
|
||||
*/
|
||||
texturePosition: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
texturePositionClipSpace: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in pixels.
|
||||
*/
|
||||
size: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
sizeClipSpace: IVector;
|
||||
}
|
||||
|
||||
export interface IVector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IBoundingBox {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export interface ISelectionRenderModel {
|
||||
readonly hasSelection: boolean;
|
||||
readonly columnSelectMode: boolean;
|
||||
readonly viewportStartRow: number;
|
||||
readonly viewportEndRow: number;
|
||||
readonly viewportCappedStartRow: number;
|
||||
readonly viewportCappedEndRow: number;
|
||||
readonly startCol: number;
|
||||
readonly endCol: number;
|
||||
readonly selectionStart: [number, number] | undefined;
|
||||
readonly selectionEnd: [number, number] | undefined;
|
||||
clear(): void;
|
||||
update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void;
|
||||
isCellSelected(terminal: Terminal, x: number, y: number): boolean;
|
||||
}
|
||||
@@ -16,8 +16,8 @@ export class CharSizeService implements ICharSizeService {
|
||||
|
||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||
|
||||
private _onCharSizeChange = new EventEmitter<void>();
|
||||
public get onCharSizeChange(): IEvent<void> { return this._onCharSizeChange.event; }
|
||||
private readonly _onCharSizeChange = new EventEmitter<void>();
|
||||
public readonly onCharSizeChange = this._onCharSizeChange.event;
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { RenderDebouncer } from 'browser/RenderDebouncer';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
@@ -12,7 +12,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { DebouncedIdleTask } from 'common/Idle';
|
||||
import { DebouncedIdleTask } from 'common/TaskQueue';
|
||||
|
||||
interface ISelectionState {
|
||||
start: [number, number] | undefined;
|
||||
@@ -40,14 +40,14 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
columnSelectMode: false
|
||||
};
|
||||
|
||||
private _onDimensionsChange = new EventEmitter<IRenderDimensions>();
|
||||
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._onDimensionsChange.event; }
|
||||
private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; }
|
||||
private _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; }
|
||||
private readonly _onDimensionsChange = new EventEmitter<IRenderDimensions>();
|
||||
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
||||
private readonly _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
|
||||
public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
|
||||
private readonly _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public readonly onRender = this._onRender.event;
|
||||
private readonly _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
|
||||
public readonly onRefreshRequest = this._onRefreshRequest.event;
|
||||
|
||||
public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; }
|
||||
|
||||
|
||||
@@ -111,14 +111,14 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
private _oldSelectionStart: [number, number] | undefined = undefined;
|
||||
private _oldSelectionEnd: [number, number] | undefined = undefined;
|
||||
|
||||
private _onLinuxMouseSelection = this.register(new EventEmitter<string>());
|
||||
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
|
||||
private _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>());
|
||||
public get onRequestRedraw(): IEvent<ISelectionRedrawRequestEvent> { return this._onRedrawRequest.event; }
|
||||
private _onSelectionChange = this.register(new EventEmitter<void>());
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
private _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>());
|
||||
public get onRequestScrollLines(): IEvent<ISelectionRequestScrollLinesEvent> { return this._onRequestScrollLines.event; }
|
||||
private readonly _onLinuxMouseSelection = this.register(new EventEmitter<string>());
|
||||
public readonly onLinuxMouseSelection = this._onLinuxMouseSelection.event;
|
||||
private readonly _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>());
|
||||
public readonly onRequestRedraw = this._onRedrawRequest.event;
|
||||
private readonly _onSelectionChange = this.register(new EventEmitter<void>());
|
||||
public readonly onSelectionChange = this._onSelectionChange.event;
|
||||
private readonly _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>());
|
||||
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
|
||||
|
||||
constructor(
|
||||
private readonly _element: HTMLElement,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
|
||||
@@ -25,12 +25,12 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
private _startIndex: number;
|
||||
private _length: number;
|
||||
|
||||
public onDeleteEmitter = new EventEmitter<IDeleteEvent>();
|
||||
public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; }
|
||||
public onInsertEmitter = new EventEmitter<IInsertEvent>();
|
||||
public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; }
|
||||
public onTrimEmitter = new EventEmitter<number>();
|
||||
public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; }
|
||||
public readonly onDeleteEmitter = new EventEmitter<IDeleteEvent>();
|
||||
public readonly onDelete = this.onDeleteEmitter.event;
|
||||
public readonly onInsertEmitter = new EventEmitter<IInsertEvent>();
|
||||
public readonly onInsert = this.onInsertEmitter.event;
|
||||
public readonly onTrimEmitter = new EventEmitter<number>();
|
||||
public readonly onTrim = this.onTrimEmitter.event;
|
||||
|
||||
constructor(
|
||||
private _maxLength: number
|
||||
|
||||
+6
-1
@@ -11,6 +11,11 @@ let $g = 0;
|
||||
let $b = 0;
|
||||
let $a = 0;
|
||||
|
||||
export const NULL_COLOR: IColor = {
|
||||
css: '#00000000',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper functions where the source type is "channels" (individual color channels as numbers).
|
||||
*/
|
||||
@@ -129,7 +134,7 @@ export namespace css {
|
||||
*/
|
||||
export function toColor(css: string): IColor {
|
||||
// Formats: #rgb[a] and #rrggbb[aa]
|
||||
if (css.match(/#[0-9a-f]{3,8}/i)) {
|
||||
if (css.match(/#[\da-f]{3,8}/i)) {
|
||||
switch (css.length) {
|
||||
case 4: { // #rgb
|
||||
$r = parseInt(css.slice(1, 2).repeat(2), 16);
|
||||
|
||||
+16
-18
@@ -22,7 +22,7 @@
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
|
||||
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
|
||||
import { InstantiationService } from 'common/services/InstantiationService';
|
||||
import { LogService } from 'common/services/LogService';
|
||||
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
|
||||
@@ -31,7 +31,6 @@ import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource
|
||||
import { CoreService } from 'common/services/CoreService';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
import { DirtyRowService } from 'common/services/DirtyRowService';
|
||||
import { UnicodeService } from 'common/services/UnicodeService';
|
||||
import { CharsetService } from 'common/services/CharsetService';
|
||||
import { updateWindowsModeWrappedState } from 'common/WindowsMode';
|
||||
@@ -49,7 +48,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
protected readonly _bufferService: IBufferService;
|
||||
protected readonly _logService: ILogService;
|
||||
protected readonly _charsetService: ICharsetService;
|
||||
protected readonly _dirtyRowService: IDirtyRowService;
|
||||
protected readonly _oscLinkService: IOscLinkService;
|
||||
|
||||
public readonly coreMouseService: ICoreMouseService;
|
||||
@@ -61,22 +59,23 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
private _writeBuffer: WriteBuffer;
|
||||
private _windowsMode: IDisposable | undefined;
|
||||
|
||||
private _onBinary = new EventEmitter<string>();
|
||||
public get onBinary(): IEvent<string> { return this._onBinary.event; }
|
||||
private _onData = new EventEmitter<string>();
|
||||
public get onData(): IEvent<string> { return this._onData.event; }
|
||||
private readonly _onBinary = new EventEmitter<string>();
|
||||
public readonly onBinary = this._onBinary.event;
|
||||
private readonly _onData = new EventEmitter<string>();
|
||||
public readonly onData = this._onData.event;
|
||||
protected _onLineFeed = new EventEmitter<void>();
|
||||
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
protected _onScroll = new EventEmitter<IScrollEvent, void>();
|
||||
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
|
||||
protected _onWriteParsed = new EventEmitter<void>();
|
||||
public readonly onLineFeed = this._onLineFeed.event;
|
||||
private readonly _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public readonly onResize = this._onResize.event;
|
||||
protected readonly _onWriteParsed = new EventEmitter<void>();
|
||||
public readonly onWriteParsed = this._onWriteParsed.event;
|
||||
|
||||
/**
|
||||
* Internally we track the source of the scroll but this is meaningless outside the library so
|
||||
* it's filtered out.
|
||||
*/
|
||||
protected _onScrollApi?: EventEmitter<number, void>;
|
||||
protected _onScroll = new EventEmitter<IScrollEvent, void>();
|
||||
public get onScroll(): IEvent<number, void> {
|
||||
if (!this._onScrollApi) {
|
||||
this._onScrollApi = new EventEmitter<number, void>();
|
||||
@@ -114,8 +113,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this._instantiationService.setService(ICoreService, this.coreService);
|
||||
this.coreMouseService = this._instantiationService.createInstance(CoreMouseService);
|
||||
this._instantiationService.setService(ICoreMouseService, this.coreMouseService);
|
||||
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
|
||||
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
|
||||
this.unicodeService = this._instantiationService.createInstance(UnicodeService);
|
||||
this._instantiationService.setService(IUnicodeService, this.unicodeService);
|
||||
this._charsetService = this._instantiationService.createInstance(CharsetService);
|
||||
@@ -124,7 +121,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
|
||||
|
||||
// Register input handler and handle/forward events
|
||||
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService);
|
||||
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService);
|
||||
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
|
||||
this.register(this._inputHandler);
|
||||
|
||||
@@ -132,14 +129,15 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this.register(forwardEvent(this._bufferService.onResize, this._onResize));
|
||||
this.register(forwardEvent(this.coreService.onData, this._onData));
|
||||
this.register(forwardEvent(this.coreService.onBinary, this._onBinary));
|
||||
this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));
|
||||
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
|
||||
this.register(this._bufferService.onScroll(event => {
|
||||
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
|
||||
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
}));
|
||||
this.register(this._inputHandler.onScroll(event => {
|
||||
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
|
||||
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
}));
|
||||
|
||||
// Setup WriteBuffer
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* A queue of that runs tasks over several idle callbacks, trying to maintain the specified
|
||||
* frame rate. The tasks will run in the order they are enqueued, but they will run some time later,
|
||||
* and care should be taken to ensure they're non-urgent and will not introduce race conditions.
|
||||
*/
|
||||
export class IdleTaskQueue {
|
||||
private _tasks: (() => void)[] = [];
|
||||
private _idleCallback?: number;
|
||||
private _maxTaskDuration: number;
|
||||
private _i = 0;
|
||||
|
||||
/**
|
||||
* @param targetFps The target frame rate.
|
||||
*/
|
||||
constructor(targetFps: number = 240) {
|
||||
this._maxTaskDuration = 1000 / targetFps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task to the queue which will run in a future idle callback.
|
||||
*/
|
||||
public enqueue(task: () => void): void {
|
||||
this._tasks.push(task);
|
||||
this._start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the queue, running all remaining tasks synchronously.
|
||||
*/
|
||||
public flush(): void {
|
||||
while (this._i < this._tasks.length) {
|
||||
this._tasks[this._i++]();
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any remaining tasks from the queue, these will not be run.
|
||||
*/
|
||||
public clear(): void {
|
||||
if (this._idleCallback) {
|
||||
cancelIdleCallback(this._idleCallback);
|
||||
this._idleCallback = undefined;
|
||||
}
|
||||
this._i = 0;
|
||||
this._tasks.length = 0;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
if (!this._idleCallback) {
|
||||
this._idleCallback = requestIdleCallback(() => this._process());
|
||||
}
|
||||
}
|
||||
|
||||
private _process(): void {
|
||||
const start = performance.now();
|
||||
this._idleCallback = undefined;
|
||||
while (this._i < this._tasks.length) {
|
||||
this._tasks[this._i++]();
|
||||
if (performance.now() - start > this._maxTaskDuration) {
|
||||
this._start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class DebouncedIdleTask {
|
||||
private _queue: IdleTaskQueue;
|
||||
|
||||
/**
|
||||
* @param targetFps The target frame rate.
|
||||
*/
|
||||
constructor(targetFps: number = 240) {
|
||||
this._queue = new IdleTaskQueue(targetFps);
|
||||
}
|
||||
|
||||
public set(task: () => void): void {
|
||||
this._queue.clear();
|
||||
this._queue.enqueue(task);
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
this._queue.flush();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData';
|
||||
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { Params } from 'common/parser/Params';
|
||||
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
|
||||
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
|
||||
import { IBufferService, ICoreService } from 'common/services/Services';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { clone } from 'common/Clone';
|
||||
@@ -67,7 +67,7 @@ describe('InputHandler', () => {
|
||||
bufferService.resize(80, 30);
|
||||
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
|
||||
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
|
||||
describe('SL/SR/DECIC/DECDC', () => {
|
||||
@@ -236,7 +236,7 @@ describe('InputHandler', () => {
|
||||
describe('setMode', () => {
|
||||
it('should toggle bracketedPasteMode', () => {
|
||||
const coreService = new MockCoreService();
|
||||
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
// Set bracketed paste mode
|
||||
inputHandler.setModePrivate(Params.fromArray([2004]));
|
||||
assert.equal(coreService.decPrivateModes.bracketedPasteMode, true);
|
||||
@@ -258,7 +258,6 @@ describe('InputHandler', () => {
|
||||
bufferService,
|
||||
new MockCharsetService(),
|
||||
new MockCoreService(),
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
@@ -305,7 +304,6 @@ describe('InputHandler', () => {
|
||||
bufferService,
|
||||
new MockCharsetService(),
|
||||
new MockCoreService(),
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
@@ -356,7 +354,6 @@ describe('InputHandler', () => {
|
||||
bufferService,
|
||||
new MockCharsetService(),
|
||||
new MockCoreService(),
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
@@ -394,7 +391,6 @@ describe('InputHandler', () => {
|
||||
bufferService,
|
||||
new MockCharsetService(),
|
||||
new MockCoreService(),
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
@@ -445,7 +441,6 @@ describe('InputHandler', () => {
|
||||
bufferService,
|
||||
new MockCharsetService(),
|
||||
new MockCoreService(),
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
@@ -572,7 +567,6 @@ describe('InputHandler', () => {
|
||||
new MockBufferService(80, 30),
|
||||
new MockCharsetService(),
|
||||
new MockCoreService(),
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
@@ -599,7 +593,7 @@ describe('InputHandler', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
bufferService = new MockBufferService(80, 30);
|
||||
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => {
|
||||
await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
|
||||
@@ -796,7 +790,7 @@ describe('InputHandler', () => {
|
||||
describe('colon notation', () => {
|
||||
let inputHandler2: TestInputHandler;
|
||||
beforeEach(() => {
|
||||
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
describe('should equal to semicolon', () => {
|
||||
it('CSI 38:2::50:100:150 m', async () => {
|
||||
@@ -2278,7 +2272,7 @@ describe('InputHandler - async handlers', () => {
|
||||
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
|
||||
coreService.onData(data => { console.log(data); });
|
||||
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
|
||||
it('async CUP with CPR check', async () => {
|
||||
|
||||
+117
-58
@@ -12,11 +12,11 @@ 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 { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
|
||||
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';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
|
||||
import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
|
||||
import { OscHandler } from 'common/parser/OscParser';
|
||||
import { DcsHandler } from 'common/parser/DcsParser';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
@@ -104,6 +104,8 @@ export enum WindowsOptionsReportType {
|
||||
// create a warning log if an async handler takes longer than the limit (in ms)
|
||||
const SLOW_ASYNC_LIMIT = 5000;
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $temp = 0;
|
||||
|
||||
/**
|
||||
* The terminal's standard implementation of IInputHandler, this handles all
|
||||
@@ -120,6 +122,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
private _windowTitle = '';
|
||||
private _iconName = '';
|
||||
private _currentLinkId?: number;
|
||||
private _dirtyRowTracker: IDirtyRowTracker;
|
||||
protected _windowTitleStack: string[] = [];
|
||||
protected _iconNameStack: string[] = [];
|
||||
|
||||
@@ -129,33 +132,33 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
private _activeBuffer: IBuffer;
|
||||
|
||||
private _onRequestBell = new EventEmitter<void>();
|
||||
public get onRequestBell(): IEvent<void> { return this._onRequestBell.event; }
|
||||
private _onRequestRefreshRows = new EventEmitter<number, number>();
|
||||
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
|
||||
private _onRequestReset = new EventEmitter<void>();
|
||||
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
|
||||
private _onRequestSendFocus = new EventEmitter<void>();
|
||||
public get onRequestSendFocus(): IEvent<void> { return this._onRequestSendFocus.event; }
|
||||
private _onRequestSyncScrollBar = new EventEmitter<void>();
|
||||
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
|
||||
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
|
||||
public get onRequestWindowsOptionsReport(): IEvent<WindowsOptionsReportType> { return this._onRequestWindowsOptionsReport.event; }
|
||||
private readonly _onRequestBell = new EventEmitter<void>();
|
||||
public readonly onRequestBell = this._onRequestBell.event;
|
||||
private readonly _onRequestRefreshRows = new EventEmitter<number, number>();
|
||||
public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;
|
||||
private readonly _onRequestReset = new EventEmitter<void>();
|
||||
public readonly onRequestReset = this._onRequestReset.event;
|
||||
private readonly _onRequestSendFocus = new EventEmitter<void>();
|
||||
public readonly onRequestSendFocus = this._onRequestSendFocus.event;
|
||||
private readonly _onRequestSyncScrollBar = new EventEmitter<void>();
|
||||
public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;
|
||||
private readonly _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
|
||||
public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;
|
||||
|
||||
private _onA11yChar = new EventEmitter<string>();
|
||||
public get onA11yChar(): IEvent<string> { return this._onA11yChar.event; }
|
||||
private _onA11yTab = new EventEmitter<number>();
|
||||
public get onA11yTab(): IEvent<number> { return this._onA11yTab.event; }
|
||||
private _onCursorMove = new EventEmitter<void>();
|
||||
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
|
||||
private _onLineFeed = new EventEmitter<void>();
|
||||
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
|
||||
private _onScroll = new EventEmitter<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
private _onTitleChange = new EventEmitter<string>();
|
||||
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
|
||||
private _onColor = new EventEmitter<IColorEvent>();
|
||||
public get onColor(): IEvent<IColorEvent> { return this._onColor.event; }
|
||||
private readonly _onA11yChar = new EventEmitter<string>();
|
||||
public readonly onA11yChar = this._onA11yChar.event;
|
||||
private readonly _onA11yTab = new EventEmitter<number>();
|
||||
public readonly onA11yTab = this._onA11yTab.event;
|
||||
private readonly _onCursorMove = new EventEmitter<void>();
|
||||
public readonly onCursorMove = this._onCursorMove.event;
|
||||
private readonly _onLineFeed = new EventEmitter<void>();
|
||||
public readonly onLineFeed = this._onLineFeed.event;
|
||||
private readonly _onScroll = new EventEmitter<number>();
|
||||
public readonly onScroll = this._onScroll.event;
|
||||
private readonly _onTitleChange = new EventEmitter<string>();
|
||||
public readonly onTitleChange = this._onTitleChange.event;
|
||||
private readonly _onColor = new EventEmitter<IColorEvent>();
|
||||
public readonly onColor = this._onColor.event;
|
||||
|
||||
private _parseStack: IParseStack = {
|
||||
paused: false,
|
||||
@@ -169,7 +172,6 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
private readonly _bufferService: IBufferService,
|
||||
private readonly _charsetService: ICharsetService,
|
||||
private readonly _coreService: ICoreService,
|
||||
private readonly _dirtyRowService: IDirtyRowService,
|
||||
private readonly _logService: ILogService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _oscLinkService: IOscLinkService,
|
||||
@@ -179,6 +181,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
) {
|
||||
super();
|
||||
this.register(this._parser);
|
||||
this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);
|
||||
|
||||
// Track properties used in performance critical code manually to avoid using slow getters
|
||||
this._activeBuffer = this._bufferService.buffer;
|
||||
@@ -459,7 +462,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// Clear the dirty row service so we know which lines changed as a result of parsing
|
||||
// Important: do not clear between async calls, otherwise we lost pending update information.
|
||||
if (!wasPaused) {
|
||||
this._dirtyRowService.clearRange();
|
||||
this._dirtyRowTracker.clearRange();
|
||||
}
|
||||
|
||||
// process big data in smaller chunks
|
||||
@@ -493,7 +496,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
|
||||
// Refresh any dirty rows accumulated as part of parsing
|
||||
this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end);
|
||||
this._onRequestRefreshRows.fire(this._dirtyRowTracker.start, this._dirtyRowTracker.end);
|
||||
}
|
||||
|
||||
public print(data: Uint32Array, start: number, end: number): void {
|
||||
@@ -507,7 +510,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
const curAttr = this._curAttrData;
|
||||
let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
|
||||
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
|
||||
// handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char
|
||||
if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {
|
||||
@@ -635,7 +638,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
|
||||
}
|
||||
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -699,7 +702,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF."
|
||||
*/
|
||||
public lineFeed(): boolean {
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
if (this._optionsService.rawOptions.convertEol) {
|
||||
this._activeBuffer.x = 0;
|
||||
}
|
||||
@@ -714,7 +717,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (this._activeBuffer.x >= this._bufferService.cols) {
|
||||
this._activeBuffer.x--;
|
||||
}
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
|
||||
this._onLineFeed.fire();
|
||||
return true;
|
||||
@@ -842,14 +845,14 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.y = this._coreService.decPrivateModes.origin
|
||||
? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))
|
||||
: Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set absolute cursor position.
|
||||
*/
|
||||
private _setCursor(x: number, y: number): void {
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
if (this._coreService.decPrivateModes.origin) {
|
||||
this._activeBuffer.x = x;
|
||||
this._activeBuffer.y = this._activeBuffer.scrollTop + y;
|
||||
@@ -858,7 +861,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.y = y;
|
||||
}
|
||||
this._restrictCursor();
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1178,16 +1181,16 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
switch (params.params[0]) {
|
||||
case 0:
|
||||
j = this._activeBuffer.y;
|
||||
this._dirtyRowService.markDirty(j);
|
||||
this._dirtyRowTracker.markDirty(j);
|
||||
this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);
|
||||
for (; j < this._bufferService.rows; j++) {
|
||||
this._resetBufferLine(j, respectProtect);
|
||||
}
|
||||
this._dirtyRowService.markDirty(j);
|
||||
this._dirtyRowTracker.markDirty(j);
|
||||
break;
|
||||
case 1:
|
||||
j = this._activeBuffer.y;
|
||||
this._dirtyRowService.markDirty(j);
|
||||
this._dirtyRowTracker.markDirty(j);
|
||||
// Deleted front part of line and everything before. This line will no longer be wrapped.
|
||||
this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);
|
||||
if (this._activeBuffer.x + 1 >= this._bufferService.cols) {
|
||||
@@ -1197,15 +1200,15 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
while (j--) {
|
||||
this._resetBufferLine(j, respectProtect);
|
||||
}
|
||||
this._dirtyRowService.markDirty(0);
|
||||
this._dirtyRowTracker.markDirty(0);
|
||||
break;
|
||||
case 2:
|
||||
j = this._bufferService.rows;
|
||||
this._dirtyRowService.markDirty(j - 1);
|
||||
this._dirtyRowTracker.markDirty(j - 1);
|
||||
while (j--) {
|
||||
this._resetBufferLine(j, respectProtect);
|
||||
}
|
||||
this._dirtyRowService.markDirty(0);
|
||||
this._dirtyRowTracker.markDirty(0);
|
||||
break;
|
||||
case 3:
|
||||
// Clear scrollback (everything not in viewport)
|
||||
@@ -1257,7 +1260,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);
|
||||
break;
|
||||
}
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1289,7 +1292,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
|
||||
}
|
||||
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
|
||||
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
|
||||
return true;
|
||||
}
|
||||
@@ -1323,7 +1326,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
|
||||
}
|
||||
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
|
||||
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
|
||||
return true;
|
||||
}
|
||||
@@ -1349,7 +1352,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.getNullCell(this._eraseAttrData()),
|
||||
this._eraseAttrData()
|
||||
);
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1375,7 +1378,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.getNullCell(this._eraseAttrData()),
|
||||
this._eraseAttrData()
|
||||
);
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1395,7 +1398,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);
|
||||
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
|
||||
}
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1411,7 +1414,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);
|
||||
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1443,7 +1446,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
|
||||
line.isWrapped = false;
|
||||
}
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1476,7 +1479,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
|
||||
line.isWrapped = false;
|
||||
}
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1499,7 +1502,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
|
||||
line.isWrapped = false;
|
||||
}
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1522,7 +1525,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
|
||||
line.isWrapped = false;
|
||||
}
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1544,7 +1547,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._activeBuffer.getNullCell(this._eraseAttrData()),
|
||||
this._eraseAttrData()
|
||||
);
|
||||
this._dirtyRowService.markDirty(this._activeBuffer.y);
|
||||
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -3220,7 +3223,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;
|
||||
this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);
|
||||
this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));
|
||||
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
} else {
|
||||
this._activeBuffer.y--;
|
||||
this._restrictCursor(); // quickfix to not run out of bounds
|
||||
@@ -3293,7 +3296,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
line.isWrapped = false;
|
||||
}
|
||||
}
|
||||
this._dirtyRowService.markAllDirty();
|
||||
this._dirtyRowTracker.markAllDirty();
|
||||
this._setCursor(0, 0);
|
||||
return true;
|
||||
}
|
||||
@@ -3344,4 +3347,60 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);
|
||||
return f(`P0$r`);
|
||||
}
|
||||
|
||||
public markRangeDirty(y1: number, y2: number): void {
|
||||
this._dirtyRowTracker.markRangeDirty(y1, y2);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDirtyRowTracker {
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
|
||||
clearRange(): void;
|
||||
markDirty(y: number): void;
|
||||
markRangeDirty(y1: number, y2: number): void;
|
||||
markAllDirty(): void;
|
||||
}
|
||||
|
||||
class DirtyRowTracker implements IDirtyRowTracker {
|
||||
public start!: number;
|
||||
public end!: number;
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
this.clearRange();
|
||||
}
|
||||
|
||||
public clearRange(): void {
|
||||
this.start = this._bufferService.buffer.y;
|
||||
this.end = this._bufferService.buffer.y;
|
||||
}
|
||||
|
||||
public markDirty(y: number): void {
|
||||
if (y < this.start) {
|
||||
this.start = y;
|
||||
} else if (y > this.end) {
|
||||
this.end = y;
|
||||
}
|
||||
}
|
||||
|
||||
public markRangeDirty(y1: number, y2: number): void {
|
||||
if (y1 > y2) {
|
||||
$temp = y1;
|
||||
y1 = y2;
|
||||
y2 = $temp;
|
||||
}
|
||||
if (y1 < this.start) {
|
||||
this.start = y1;
|
||||
}
|
||||
if (y2 > this.end) {
|
||||
this.end = y2;
|
||||
}
|
||||
}
|
||||
|
||||
public markAllDirty(): void {
|
||||
this.markRangeDirty(0, this._bufferService.rows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { isNode } from 'common/Platform';
|
||||
|
||||
interface ITaskQueue {
|
||||
/**
|
||||
* Adds a task to the queue which will run in a future idle callback.
|
||||
*/
|
||||
enqueue(task: () => void): void;
|
||||
|
||||
/**
|
||||
* Flushes the queue, running all remaining tasks synchronously.
|
||||
*/
|
||||
flush(): void;
|
||||
|
||||
/**
|
||||
* Clears any remaining tasks from the queue, these will not be run.
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
interface ITaskDeadline {
|
||||
timeRemaining(): number;
|
||||
}
|
||||
type CallbackWithDeadline = (deadline: ITaskDeadline) => void;
|
||||
|
||||
abstract class TaskQueue implements ITaskQueue {
|
||||
private _tasks: (() => void)[] = [];
|
||||
private _idleCallback?: number;
|
||||
private _i = 0;
|
||||
|
||||
protected abstract _requestCallback(callback: CallbackWithDeadline): number;
|
||||
protected abstract _cancelCallback(identifier: number): void;
|
||||
|
||||
public enqueue(task: () => void): void {
|
||||
this._tasks.push(task);
|
||||
this._start();
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
while (this._i < this._tasks.length) {
|
||||
this._tasks[this._i++]();
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
if (this._idleCallback) {
|
||||
this._cancelCallback(this._idleCallback);
|
||||
this._idleCallback = undefined;
|
||||
}
|
||||
this._i = 0;
|
||||
this._tasks.length = 0;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
if (!this._idleCallback) {
|
||||
this._idleCallback = this._requestCallback(this._process.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
private _process(deadline: ITaskDeadline): void {
|
||||
this._idleCallback = undefined;
|
||||
let taskDuration = 0;
|
||||
let longestTask = 0;
|
||||
while (this._i < this._tasks.length) {
|
||||
taskDuration = performance.now();
|
||||
this._tasks[this._i++]();
|
||||
taskDuration = performance.now() - taskDuration;
|
||||
longestTask = Math.max(taskDuration, longestTask);
|
||||
// Guess the following task will take a similar time to the longest task in this batch, allow
|
||||
// additional room to try avoid exceeding the deadline
|
||||
if (longestTask * 1.5 > deadline.timeRemaining()) {
|
||||
this._start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames
|
||||
* per second. The tasks will run in the order they are enqueued, but they will run some time later,
|
||||
* and care should be taken to ensure they're non-urgent and will not introduce race conditions.
|
||||
*/
|
||||
export class PriorityTaskQueue extends TaskQueue {
|
||||
protected _requestCallback(callback: CallbackWithDeadline): number {
|
||||
return setTimeout(() => callback(this._createDeadline(16)));
|
||||
}
|
||||
|
||||
protected _cancelCallback(identifier: number): void {
|
||||
clearTimeout(identifier);
|
||||
}
|
||||
|
||||
private _createDeadline(duration: number): ITaskDeadline {
|
||||
const end = performance.now() + duration;
|
||||
return {
|
||||
timeRemaining: () => Math.max(0, end - performance.now())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class IdleTaskQueueInternal extends TaskQueue {
|
||||
protected _requestCallback(callback: IdleRequestCallback): number {
|
||||
return requestIdleCallback(callback);
|
||||
}
|
||||
|
||||
protected _cancelCallback(identifier: number): void {
|
||||
cancelIdleCallback(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's
|
||||
* deadline given by the environment. The tasks will run in the order they are enqueued, but they
|
||||
* will run some time later, and care should be taken to ensure they're non-urgent and will not
|
||||
* introduce race conditions.
|
||||
*
|
||||
* This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? IdleTaskQueueInternal : PriorityTaskQueue;
|
||||
|
||||
/**
|
||||
* An object that tracks a single debounced task that will run on the next idle frame. When called
|
||||
* multiple times, only the last set task will run.
|
||||
*/
|
||||
export class DebouncedIdleTask {
|
||||
private _queue: ITaskQueue;
|
||||
|
||||
constructor() {
|
||||
this._queue = new IdleTaskQueue();
|
||||
}
|
||||
|
||||
public set(task: () => void): void {
|
||||
this._queue.clear();
|
||||
this._queue.enqueue(task);
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
this._queue.flush();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
@@ -100,16 +100,6 @@ export class MockCoreService implements ICoreService {
|
||||
public triggerBinaryEvent(data: string): void { }
|
||||
}
|
||||
|
||||
export class MockDirtyRowService implements IDirtyRowService {
|
||||
public serviceBrand: any;
|
||||
public start: number = 0;
|
||||
public end: number = 0;
|
||||
public clearRange(): void { }
|
||||
public markDirty(y: number): void { }
|
||||
public markRangeDirty(y1: number, y2: number): void { }
|
||||
public markAllDirty(): void { }
|
||||
}
|
||||
|
||||
export class MockLogService implements ILogService {
|
||||
public serviceBrand: any;
|
||||
public logLevel = LogLevelEnum.DEBUG;
|
||||
|
||||
@@ -37,10 +37,8 @@ const enum Cell {
|
||||
|
||||
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { startIndex: number } = {
|
||||
startIndex: 0
|
||||
};
|
||||
// Work variables to avoid garbage collection
|
||||
let $startIndex = 0;
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
@@ -178,10 +176,10 @@ export class BufferLine implements IBufferLine {
|
||||
* to GC as it significantly reduced the amount of new objects/references needed.
|
||||
*/
|
||||
public loadCell(index: number, cell: ICellData): ICellData {
|
||||
w.startIndex = index * CELL_SIZE;
|
||||
cell.content = this._data[w.startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[w.startIndex + Cell.FG];
|
||||
cell.bg = this._data[w.startIndex + Cell.BG];
|
||||
$startIndex = index * CELL_SIZE;
|
||||
cell.content = this._data[$startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[$startIndex + Cell.FG];
|
||||
cell.bg = this._data[$startIndex + Cell.BG];
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
cell.combinedData = this._combined[index];
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ export class BufferSet extends Disposable implements IBufferSet {
|
||||
private _alt!: Buffer;
|
||||
private _activeBuffer!: Buffer;
|
||||
|
||||
private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
|
||||
public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; }
|
||||
private readonly _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
|
||||
public readonly onBufferActivate = this._onBufferActivate.event;
|
||||
|
||||
/**
|
||||
* Create a new BufferSet for the given terminal.
|
||||
|
||||
@@ -15,8 +15,8 @@ export class Marker extends Disposable implements IMarker {
|
||||
|
||||
public get id(): number { return this._id; }
|
||||
|
||||
private _onDispose = new EventEmitter<void>();
|
||||
public get onDispose(): IEvent<void> { return this._onDispose.event; }
|
||||
private readonly _onDispose = new EventEmitter<void>();
|
||||
public readonly onDispose = this._onDispose.event;
|
||||
|
||||
constructor(
|
||||
public line: number
|
||||
|
||||
@@ -121,35 +121,42 @@ describe('text encodings', () => {
|
||||
|
||||
describe('Utf8ToUtf32 decoder', () => {
|
||||
describe('full codepoint test', () => {
|
||||
|
||||
it('0..65535 (1/2/3 byte sequences)', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = 0; i < 65536; ++i) {
|
||||
// skip surrogate pairs and a BOM
|
||||
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
|
||||
continue;
|
||||
function formatRange(min: number, max: number): string {
|
||||
return `${min}..${max} (0x${min.toString(16).toUpperCase()}..0x${max.toString(16).toUpperCase()})`;
|
||||
}
|
||||
for (let min = 0; min < 65535; min += 10000) {
|
||||
const max = Math.min(min + 10000, 65536);
|
||||
it(`${formatRange(min, max)} (1/2/3 byte sequences)`, () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = min; i < max; ++i) {
|
||||
// skip surrogate pairs and a BOM
|
||||
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
|
||||
continue;
|
||||
}
|
||||
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 1);
|
||||
assert.equal(toString(target, length), String.fromCharCode(i));
|
||||
decoder.clear();
|
||||
}
|
||||
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 1);
|
||||
assert.equal(toString(target, length), String.fromCharCode(i));
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('65536..0x10FFFF (4 byte sequences)', function (): void {
|
||||
this.timeout(20000);
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = 65536; i < 0x10FFFF; ++i) {
|
||||
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 1);
|
||||
assert.equal(target[0], i);
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
for (let minRaw = 60000; minRaw < 0x10FFFF; minRaw += 10000) {
|
||||
const min = Math.max(minRaw, 65536);
|
||||
const max = Math.min(minRaw + 10000, 0x10FFFF);
|
||||
it(`${formatRange(min, max)} (4 byte sequences)`, function (): void {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = min; i < max; ++i) {
|
||||
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 1);
|
||||
assert.equal(target[0], i);
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('0xFEFF(BOM)', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
|
||||
@@ -33,12 +33,6 @@ const WRITE_TIMEOUT_MS = 12;
|
||||
*/
|
||||
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
|
||||
|
||||
// queueMicrotask polyfill for nodejs < v11
|
||||
const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined')
|
||||
? (cb: () => void) => { Promise.resolve().then(cb); }
|
||||
: queueMicrotask;
|
||||
|
||||
|
||||
export class WriteBuffer {
|
||||
private _writeBuffer: (string | Uint8Array)[] = [];
|
||||
private _callbacks: ((() => void) | undefined)[] = [];
|
||||
@@ -46,11 +40,17 @@ export class WriteBuffer {
|
||||
private _bufferOffset = 0;
|
||||
private _isSyncWriting = false;
|
||||
private _syncCalls = 0;
|
||||
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
|
||||
private _onWriteParsed = new EventEmitter<void>();
|
||||
private _didUserInput = false;
|
||||
|
||||
private readonly _onWriteParsed = new EventEmitter<void>();
|
||||
public readonly onWriteParsed = this._onWriteParsed.event;
|
||||
|
||||
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
|
||||
|
||||
public handleUserInput(): void {
|
||||
this._didUserInput = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Unreliable, to be removed soon.
|
||||
*/
|
||||
@@ -105,6 +105,19 @@ export class WriteBuffer {
|
||||
// schedule chunk processing for next event loop run
|
||||
if (!this._writeBuffer.length) {
|
||||
this._bufferOffset = 0;
|
||||
|
||||
// If this is the first write call after the user has done some input,
|
||||
// parse it immediately to minimize input latency,
|
||||
// otherwise schedule for the next event
|
||||
if (this._didUserInput) {
|
||||
this._didUserInput = false;
|
||||
this._pendingData += data.length;
|
||||
this._writeBuffer.push(data);
|
||||
this._callbacks.push(callback);
|
||||
this._innerWrite();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => this._innerWrite());
|
||||
}
|
||||
|
||||
@@ -194,7 +207,7 @@ export class WriteBuffer {
|
||||
// 2. spawn a promise immediately resolving to `true`
|
||||
// (executed on the same queue, thus properly aligned before continuation happens)
|
||||
result.catch(err => {
|
||||
qmt(() => {throw err;});
|
||||
queueMicrotask(() => {throw err;});
|
||||
return Promise.resolve(false);
|
||||
}).then(continuation);
|
||||
return;
|
||||
|
||||
@@ -11,8 +11,9 @@ import { ICoreTerminal } from 'common/Types';
|
||||
export class BufferNamespaceApi implements IBufferNamespaceApi {
|
||||
private _normal: BufferApiView;
|
||||
private _alternate: BufferApiView;
|
||||
private _onBufferChange = new EventEmitter<IBufferApi>();
|
||||
public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; }
|
||||
|
||||
private readonly _onBufferChange = new EventEmitter<IBufferApi>();
|
||||
public readonly onBufferChange = this._onBufferChange.event;
|
||||
|
||||
constructor(private _core: ICoreTerminal) {
|
||||
this._normal = new BufferApiView(this._core.buffers.normal, 'normal');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { EventEmitter, IEventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
|
||||
|
||||
@@ -22,10 +22,10 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
/** Whether the user is scrolling (locks the scroll position) */
|
||||
public isUserScrolling: boolean = false;
|
||||
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
private _onScroll = new EventEmitter<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
private readonly _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public readonly onResize = this._onResize.event;
|
||||
private readonly _onScroll = new EventEmitter<number>();
|
||||
public readonly onScroll = this._onScroll.event;
|
||||
|
||||
public get buffer(): IBuffer { return this.buffers.active; }
|
||||
|
||||
|
||||
@@ -170,9 +170,11 @@ export class CoreMouseService implements ICoreMouseService {
|
||||
private _encodings: { [name: string]: CoreMouseEncoding } = {};
|
||||
private _activeProtocol: string = '';
|
||||
private _activeEncoding: string = '';
|
||||
private _onProtocolChange = new EventEmitter<CoreMouseEventType>();
|
||||
private _lastEvent: ICoreMouseEvent | null = null;
|
||||
|
||||
private readonly _onProtocolChange = new EventEmitter<CoreMouseEventType>();
|
||||
public readonly onProtocolChange = this._onProtocolChange.event;
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
@@ -225,13 +227,6 @@ export class CoreMouseService implements ICoreMouseService {
|
||||
this._lastEvent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event to announce changes in mouse tracking.
|
||||
*/
|
||||
public get onProtocolChange(): IEvent<CoreMouseEventType> {
|
||||
return this._onProtocolChange.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a mouse event to be sent.
|
||||
*
|
||||
|
||||
@@ -34,12 +34,12 @@ export class CoreService extends Disposable implements ICoreService {
|
||||
// Circular dependency, this must be unset or memory will leak after Terminal.dispose
|
||||
private _scrollToBottom: (() => void) | undefined;
|
||||
|
||||
private _onData = this.register(new EventEmitter<string>());
|
||||
public get onData(): IEvent<string> { return this._onData.event; }
|
||||
private _onUserInput = this.register(new EventEmitter<void>());
|
||||
public get onUserInput(): IEvent<void> { return this._onUserInput.event; }
|
||||
private _onBinary = this.register(new EventEmitter<string>());
|
||||
public get onBinary(): IEvent<string> { return this._onBinary.event; }
|
||||
private readonly _onData = this.register(new EventEmitter<string>());
|
||||
public readonly onData = this._onData.event;
|
||||
private readonly _onUserInput = this.register(new EventEmitter<void>());
|
||||
public readonly onUserInput = this._onUserInput.event;
|
||||
private readonly _onBinary = this.register(new EventEmitter<string>());
|
||||
public readonly onBinary = this._onBinary.event;
|
||||
|
||||
constructor(
|
||||
// TODO: Move this into a service
|
||||
|
||||
@@ -11,11 +11,9 @@ import { SortedList } from 'common/SortedList';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm';
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w = {
|
||||
xmin: 0,
|
||||
xmax: 0
|
||||
};
|
||||
// Work variables to avoid garbage collection
|
||||
let $xmin = 0;
|
||||
let $xmax = 0;
|
||||
|
||||
export class DecorationService extends Disposable implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
@@ -27,10 +25,10 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
*/
|
||||
private readonly _decorations: SortedList<IInternalDecoration> = new SortedList(e => e?.marker.line);
|
||||
|
||||
private _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
|
||||
public get onDecorationRegistered(): IEvent<IInternalDecoration> { return this._onDecorationRegistered.event; }
|
||||
private _onDecorationRemoved = this.register(new EventEmitter<IInternalDecoration>());
|
||||
public get onDecorationRemoved(): IEvent<IInternalDecoration> { return this._onDecorationRemoved.event; }
|
||||
private readonly _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
|
||||
public readonly onDecorationRegistered = this._onDecorationRegistered.event;
|
||||
private readonly _onDecorationRemoved = this.register(new EventEmitter<IInternalDecoration>());
|
||||
public readonly onDecorationRemoved = this._onDecorationRemoved.event;
|
||||
|
||||
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
|
||||
|
||||
@@ -76,9 +74,9 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
|
||||
this._decorations.forEachByKey(line, d => {
|
||||
w.xmin = d.options.x ?? 0;
|
||||
w.xmax = w.xmin + (d.options.width ?? 1);
|
||||
if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
$xmin = d.options.x ?? 0;
|
||||
$xmax = $xmin + (d.options.width ?? 1);
|
||||
if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
callback(d);
|
||||
}
|
||||
});
|
||||
@@ -99,7 +97,7 @@ class Decoration extends Disposable implements IInternalDecoration {
|
||||
|
||||
public readonly onRenderEmitter = this.register(new EventEmitter<HTMLElement>());
|
||||
public readonly onRender = this.onRenderEmitter.event;
|
||||
private _onDispose = this.register(new EventEmitter<void>());
|
||||
private readonly _onDispose = this.register(new EventEmitter<void>());
|
||||
public readonly onDispose = this._onDispose.event;
|
||||
|
||||
private _cachedBg: IColor | undefined | null = null;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService, IDirtyRowService } from 'common/services/Services';
|
||||
|
||||
export class DirtyRowService implements IDirtyRowService {
|
||||
public serviceBrand: any;
|
||||
|
||||
private _start!: number;
|
||||
private _end!: number;
|
||||
|
||||
public get start(): number { return this._start; }
|
||||
public get end(): number { return this._end; }
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
this.clearRange();
|
||||
}
|
||||
|
||||
public clearRange(): void {
|
||||
this._start = this._bufferService.buffer.y;
|
||||
this._end = this._bufferService.buffer.y;
|
||||
}
|
||||
|
||||
public markDirty(y: number): void {
|
||||
if (y < this._start) {
|
||||
this._start = y;
|
||||
} else if (y > this._end) {
|
||||
this._end = y;
|
||||
}
|
||||
}
|
||||
|
||||
public markRangeDirty(y1: number, y2: number): void {
|
||||
if (y1 > y2) {
|
||||
const temp = y1;
|
||||
y1 = y2;
|
||||
y2 = temp;
|
||||
}
|
||||
if (y1 < this._start) {
|
||||
this._start = y1;
|
||||
}
|
||||
if (y2 > this._end) {
|
||||
this._end = y2;
|
||||
}
|
||||
}
|
||||
|
||||
public markAllDirty(): void {
|
||||
this.markRangeDirty(0, this._bufferService.rows - 1);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,9 @@ export class ServiceCollection {
|
||||
}
|
||||
|
||||
public forEach(callback: (id: IServiceIdentifier<any>, instance: any) => any): void {
|
||||
this._entries.forEach((value, key) => callback(key, value));
|
||||
for (const [key, value] of this._entries.entries()) {
|
||||
callback(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public has(id: IServiceIdentifier<any>): boolean {
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('OptionsService', () => {
|
||||
});
|
||||
it('normalizes invalid fontWeight option values', () => {
|
||||
service.options.fontWeight = 350;
|
||||
assert.doesNotThrow(() => service.options.fontWeight = 10000), 'fontWeight should be normalized instead of throwing';
|
||||
assert.doesNotThrow(() => service.options.fontWeight = 10000, 'fontWeight should be normalized instead of throwing');
|
||||
assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Values greater than 1000 should be reset to default');
|
||||
|
||||
service.options.fontWeight = 350;
|
||||
|
||||
@@ -57,8 +57,8 @@ export class OptionsService implements IOptionsService {
|
||||
public readonly rawOptions: Required<ITerminalOptions>;
|
||||
public options: Required<ITerminalOptions>;
|
||||
|
||||
private _onOptionChange = new EventEmitter<string>();
|
||||
public get onOptionChange(): IEvent<string> { return this._onOptionChange.event; }
|
||||
private readonly _onOptionChange = new EventEmitter<string>();
|
||||
public readonly onOptionChange = this._onOptionChange.event;
|
||||
|
||||
constructor(options: Partial<ITerminalOptions>) {
|
||||
// set the default value of each option
|
||||
|
||||
@@ -122,19 +122,6 @@ export interface ICharsetService {
|
||||
setgCharset(g: number, charset: ICharset | undefined): void;
|
||||
}
|
||||
|
||||
export const IDirtyRowService = createDecorator<IDirtyRowService>('DirtyRowService');
|
||||
export interface IDirtyRowService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
|
||||
clearRange(): void;
|
||||
markDirty(y: number): void;
|
||||
markRangeDirty(y1: number, y2: number): void;
|
||||
markAllDirty(): void;
|
||||
}
|
||||
|
||||
export interface IServiceIdentifier<T> {
|
||||
(...args: any[]): void;
|
||||
type: T;
|
||||
|
||||
@@ -13,8 +13,9 @@ export class UnicodeService implements IUnicodeService {
|
||||
private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);
|
||||
private _active: string = '';
|
||||
private _activeProvider: IUnicodeVersionProvider;
|
||||
private _onChange = new EventEmitter<string>();
|
||||
public get onChange(): IEvent<string> { return this._onChange.event; }
|
||||
|
||||
private readonly _onChange = new EventEmitter<string>();
|
||||
public readonly onChange = this._onChange.event;
|
||||
|
||||
constructor() {
|
||||
const defaultProvider = new UnicodeV6();
|
||||
|
||||
+10
-11
@@ -32,17 +32,16 @@ export class Terminal extends CoreTerminal {
|
||||
// TODO: We should remove options once components adopt optionsService
|
||||
public get options(): Required<IInitializedTerminalOptions> { return this.optionsService.options; }
|
||||
|
||||
private _onBell = new EventEmitter<void>();
|
||||
public get onBell(): IEvent<void> { return this._onBell.event; }
|
||||
private _onCursorMove = new EventEmitter<void>();
|
||||
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
|
||||
private _onTitleChange = new EventEmitter<string>();
|
||||
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
|
||||
|
||||
private _onA11yCharEmitter = new EventEmitter<string>();
|
||||
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
|
||||
private _onA11yTabEmitter = new EventEmitter<number>();
|
||||
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
|
||||
private readonly _onBell = new EventEmitter<void>();
|
||||
public readonly onBell = this._onBell.event;
|
||||
private readonly _onCursorMove = new EventEmitter<void>();
|
||||
public readonly onCursorMove = this._onCursorMove.event;
|
||||
private readonly _onTitleChange = new EventEmitter<string>();
|
||||
public readonly onTitleChange = this._onTitleChange.event;
|
||||
private readonly _onA11yCharEmitter = new EventEmitter<string>();
|
||||
public readonly onA11yChar = this._onA11yCharEmitter.event;
|
||||
private readonly _onA11yTabEmitter = new EventEmitter<number>();
|
||||
public readonly onA11yTab = this._onA11yTabEmitter.event;
|
||||
|
||||
/**
|
||||
* Creates a new `Terminal` object.
|
||||
|
||||
Reference in New Issue
Block a user